github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/services/notary/notary.go (about) 1 package notary 2 3 import ( 4 "bytes" 5 "crypto/elliptic" 6 "encoding/hex" 7 "errors" 8 "fmt" 9 "sync" 10 "sync/atomic" 11 12 "github.com/nspcc-dev/neo-go/pkg/config" 13 "github.com/nspcc-dev/neo-go/pkg/config/netmode" 14 "github.com/nspcc-dev/neo-go/pkg/core/block" 15 "github.com/nspcc-dev/neo-go/pkg/core/mempool" 16 "github.com/nspcc-dev/neo-go/pkg/core/mempoolevent" 17 "github.com/nspcc-dev/neo-go/pkg/core/transaction" 18 "github.com/nspcc-dev/neo-go/pkg/crypto/hash" 19 "github.com/nspcc-dev/neo-go/pkg/crypto/keys" 20 "github.com/nspcc-dev/neo-go/pkg/io" 21 "github.com/nspcc-dev/neo-go/pkg/network/payload" 22 "github.com/nspcc-dev/neo-go/pkg/util" 23 "github.com/nspcc-dev/neo-go/pkg/vm" 24 "github.com/nspcc-dev/neo-go/pkg/vm/opcode" 25 "github.com/nspcc-dev/neo-go/pkg/wallet" 26 "go.uber.org/zap" 27 ) 28 29 type ( 30 // Ledger is the interface to Blockchain sufficient for Notary. 31 Ledger interface { 32 BlockHeight() uint32 33 GetMaxVerificationGAS() int64 34 GetNotaryContractScriptHash() util.Uint160 35 SubscribeForBlocks(ch chan *block.Block) 36 UnsubscribeFromBlocks(ch chan *block.Block) 37 VerifyWitness(util.Uint160, hash.Hashable, *transaction.Witness, int64) (int64, error) 38 } 39 40 // Notary represents a Notary module. 41 Notary struct { 42 Config Config 43 44 Network netmode.Magic 45 46 // onTransaction is a callback for completed transactions (mains or fallbacks) sending. 47 onTransaction func(tx *transaction.Transaction) error 48 // newTxs is a channel where new transactions are sent 49 // to be processed in an `onTransaction` callback. 50 newTxs chan txHashPair 51 // started is a status bool to protect from double start/shutdown. 52 started atomic.Bool 53 54 // reqMtx protects requests list. 55 reqMtx sync.RWMutex 56 // requests represents a map of main transactions which needs to be completed 57 // with the associated fallback transactions grouped by the main transaction hash 58 requests map[util.Uint256]*request 59 60 // accMtx protects account. 61 accMtx sync.RWMutex 62 currAccount *wallet.Account 63 wallet *wallet.Wallet 64 65 mp *mempool.Pool 66 // requests channel 67 reqCh chan mempoolevent.Event 68 // blocksCh is a channel used to receive block notifications from the 69 // Blockchain. It is not buffered intentionally, as it's important to keep 70 // the notary request pool in sync with the current blockchain heigh, thus, 71 // it's not recommended to use a large size of notary requests pool as it may 72 // slow down the block processing. 73 blocksCh chan *block.Block 74 stopCh chan struct{} 75 done chan struct{} 76 } 77 78 // Config represents external configuration for Notary module. 79 Config struct { 80 MainCfg config.P2PNotary 81 Chain Ledger 82 Log *zap.Logger 83 } 84 ) 85 86 const defaultTxChannelCapacity = 100 87 88 type ( 89 // request represents Notary service request. 90 request struct { 91 // isSent indicates whether the main transaction was successfully sent to the network. 92 isSent bool 93 main *transaction.Transaction 94 // minNotValidBefore is the minimum NVB value among fallbacks transactions. 95 // We stop trying to send the mainTx to the network if the chain reaches the minNotValidBefore height. 96 minNotValidBefore uint32 97 fallbacks []*transaction.Transaction 98 99 witnessInfo []witnessInfo 100 } 101 102 // witnessInfo represents information about the signer and its witness. 103 witnessInfo struct { 104 typ RequestType 105 // nSigsLeft is the number of signatures left to collect to complete the main transaction. 106 // Initial nSigsLeft value is defined as following: 107 // nSigsLeft == nKeys for standard signature request; 108 // nSigsLeft <= nKeys for multisignature request; 109 nSigsLeft uint8 110 111 // sigs is a map of partial multisig invocation scripts [opcode.PUSHDATA1+64+signatureBytes] grouped by public keys. 112 sigs map[*keys.PublicKey][]byte 113 // pubs is a set of public keys participating in the multisignature witness collection. 114 pubs keys.PublicKeys 115 } 116 ) 117 118 // isMainCompleted denotes whether all signatures for the main transaction were collected. 119 func (r request) isMainCompleted() bool { 120 if r.witnessInfo == nil { 121 return false 122 } 123 for _, wi := range r.witnessInfo { 124 if wi.nSigsLeft != 0 { 125 return false 126 } 127 } 128 return true 129 } 130 131 // NewNotary returns a new Notary module. 132 func NewNotary(cfg Config, net netmode.Magic, mp *mempool.Pool, onTransaction func(tx *transaction.Transaction) error) (*Notary, error) { 133 w := cfg.MainCfg.UnlockWallet 134 wallet, err := wallet.NewWalletFromFile(w.Path) 135 if err != nil { 136 return nil, err 137 } 138 139 haveAccount := false 140 for _, acc := range wallet.Accounts { 141 if err := acc.Decrypt(w.Password, wallet.Scrypt); err == nil { 142 haveAccount = true 143 break 144 } 145 } 146 if !haveAccount { 147 return nil, errors.New("no wallet account could be unlocked") 148 } 149 150 return &Notary{ 151 requests: make(map[util.Uint256]*request), 152 Config: cfg, 153 Network: net, 154 wallet: wallet, 155 onTransaction: onTransaction, 156 newTxs: make(chan txHashPair, defaultTxChannelCapacity), 157 mp: mp, 158 reqCh: make(chan mempoolevent.Event), 159 blocksCh: make(chan *block.Block), 160 stopCh: make(chan struct{}), 161 done: make(chan struct{}), 162 }, nil 163 } 164 165 // Name returns service name. 166 func (n *Notary) Name() string { 167 return "notary" 168 } 169 170 // Start runs a Notary module in a separate goroutine. 171 // The Notary only starts once, subsequent calls to Start are no-op. 172 func (n *Notary) Start() { 173 if !n.started.CompareAndSwap(false, true) { 174 return 175 } 176 n.Config.Log.Info("starting notary service") 177 go n.newTxCallbackLoop() 178 go n.mainLoop() 179 } 180 181 func (n *Notary) mainLoop() { 182 n.Config.Chain.SubscribeForBlocks(n.blocksCh) 183 n.mp.SubscribeForTransactions(n.reqCh) 184 mainloop: 185 for { 186 select { 187 case <-n.stopCh: 188 n.mp.UnsubscribeFromTransactions(n.reqCh) 189 n.Config.Chain.UnsubscribeFromBlocks(n.blocksCh) 190 break mainloop 191 case event := <-n.reqCh: 192 if req, ok := event.Data.(*payload.P2PNotaryRequest); ok { 193 switch event.Type { 194 case mempoolevent.TransactionAdded: 195 n.OnNewRequest(req) 196 case mempoolevent.TransactionRemoved: 197 n.OnRequestRemoval(req) 198 } 199 } 200 case <-n.blocksCh: 201 // a new block was added, we need to check for valid fallbacks 202 n.PostPersist() 203 } 204 } 205 drainLoop: 206 for { 207 select { 208 case <-n.blocksCh: 209 case <-n.reqCh: 210 default: 211 break drainLoop 212 } 213 } 214 close(n.blocksCh) 215 close(n.reqCh) 216 close(n.done) 217 } 218 219 // Shutdown stops the Notary module. It can only be called once, subsequent calls 220 // to Shutdown on the same instance are no-op. The instance that was stopped can 221 // not be started again by calling Start (use a new instance if needed). 222 func (n *Notary) Shutdown() { 223 if !n.started.CompareAndSwap(true, false) { 224 return 225 } 226 n.Config.Log.Info("stopping notary service") 227 close(n.stopCh) 228 <-n.done 229 n.wallet.Close() 230 _ = n.Config.Log.Sync() 231 } 232 233 // IsAuthorized returns whether Notary service currently is authorized to collect 234 // signatures. It returnes true iff designated Notary node's account provided to 235 // the Notary service in decrypted state. 236 func (n *Notary) IsAuthorized() bool { 237 return n.getAccount() != nil 238 } 239 240 // OnNewRequest is a callback method which is called after a new notary request is added to the notary request pool. 241 func (n *Notary) OnNewRequest(payload *payload.P2PNotaryRequest) { 242 if !n.started.Load() { 243 return 244 } 245 acc := n.getAccount() 246 if acc == nil { 247 return 248 } 249 250 nvbFallback := payload.FallbackTransaction.GetAttributes(transaction.NotValidBeforeT)[0].Value.(*transaction.NotValidBefore).Height 251 nKeys := payload.MainTransaction.GetAttributes(transaction.NotaryAssistedT)[0].Value.(*transaction.NotaryAssisted).NKeys 252 newInfo, validationErr := n.verifyIncompleteWitnesses(payload.MainTransaction, nKeys) 253 if validationErr != nil { 254 n.Config.Log.Info("verification of main notary transaction failed; fallback transaction will be completed", 255 zap.String("main hash", payload.MainTransaction.Hash().StringLE()), 256 zap.String("fallback hash", payload.FallbackTransaction.Hash().StringLE()), 257 zap.String("verification error", validationErr.Error())) 258 } 259 n.reqMtx.Lock() 260 defer n.reqMtx.Unlock() 261 r, exists := n.requests[payload.MainTransaction.Hash()] 262 if exists { 263 for _, fb := range r.fallbacks { 264 if fb.Hash().Equals(payload.FallbackTransaction.Hash()) { 265 return // then we already have processed this request 266 } 267 } 268 if nvbFallback < r.minNotValidBefore { 269 r.minNotValidBefore = nvbFallback 270 } 271 } else { 272 // Avoid changes in the main transaction witnesses got from the notary request pool to 273 // keep the pooled tx valid. We will update its copy => the copy's size will be changed. 274 r = &request{ 275 main: payload.MainTransaction.Copy(), 276 minNotValidBefore: nvbFallback, 277 } 278 n.requests[payload.MainTransaction.Hash()] = r 279 } 280 if r.witnessInfo == nil && validationErr == nil { 281 r.witnessInfo = newInfo 282 } 283 // Disallow modification of a fallback transaction got from the notary 284 // request pool. Even though it has dummy Notary witness attached and its 285 // size won't be changed after finalisation, the witness bytes changes may 286 // affect the other users of notary pool and cause race. Avoid this by making 287 // the copy. 288 r.fallbacks = append(r.fallbacks, payload.FallbackTransaction.Copy()) 289 if exists && r.isMainCompleted() || validationErr != nil { 290 return 291 } 292 mainHash := hash.NetSha256(uint32(n.Network), r.main).BytesBE() 293 for i, w := range payload.MainTransaction.Scripts { 294 if len(w.InvocationScript) == 0 || // check that signature for this witness was provided 295 (r.witnessInfo[i].nSigsLeft == 0 && r.witnessInfo[i].typ != Contract) { // check that signature wasn't yet added (consider receiving the same payload multiple times) 296 continue 297 } 298 switch r.witnessInfo[i].typ { 299 case Contract: 300 // Need to check even if r.main.Scripts[i].InvocationScript is already filled in. 301 _, err := n.Config.Chain.VerifyWitness(r.main.Signers[i].Account, r.main, &w, n.Config.Chain.GetMaxVerificationGAS()) 302 if err != nil { 303 continue 304 } 305 r.main.Scripts[i].InvocationScript = w.InvocationScript 306 case Signature: 307 if r.witnessInfo[i].pubs[0].Verify(w.InvocationScript[2:], mainHash) { 308 r.main.Scripts[i] = w 309 r.witnessInfo[i].nSigsLeft-- 310 } 311 case MultiSignature: 312 if r.witnessInfo[i].sigs == nil { 313 r.witnessInfo[i].sigs = make(map[*keys.PublicKey][]byte) 314 } 315 316 for _, pub := range r.witnessInfo[i].pubs { 317 if r.witnessInfo[i].sigs[pub] != nil { 318 continue // signature for this pub has already been added 319 } 320 if pub.Verify(w.InvocationScript[2:], mainHash) { // then pub is the owner of the signature 321 r.witnessInfo[i].sigs[pub] = w.InvocationScript 322 r.witnessInfo[i].nSigsLeft-- 323 if r.witnessInfo[i].nSigsLeft == 0 { 324 var invScript []byte 325 for j := range r.witnessInfo[i].pubs { 326 if sig, ok := r.witnessInfo[i].sigs[r.witnessInfo[i].pubs[j]]; ok { 327 invScript = append(invScript, sig...) 328 } 329 } 330 r.main.Scripts[i].InvocationScript = invScript 331 } 332 break 333 } 334 } 335 // pubKey was not found for the signature (i.e. signature is bad) or the signature has already 336 // been added - we're OK with that, let the fallback TX to be added 337 } 338 } 339 if r.isMainCompleted() && r.minNotValidBefore > n.Config.Chain.BlockHeight() { 340 if err := n.finalize(acc, r.main, payload.MainTransaction.Hash()); err != nil { 341 n.Config.Log.Error("failed to finalize main transaction", 342 zap.String("hash", r.main.Hash().StringLE()), 343 zap.Error(err)) 344 } 345 } 346 } 347 348 // OnRequestRemoval is a callback which is called after fallback transaction is removed 349 // from the notary payload pool due to expiration, main tx appliance or any other reason. 350 func (n *Notary) OnRequestRemoval(pld *payload.P2PNotaryRequest) { 351 if !n.started.Load() || n.getAccount() == nil { 352 return 353 } 354 355 n.reqMtx.Lock() 356 defer n.reqMtx.Unlock() 357 r, ok := n.requests[pld.MainTransaction.Hash()] 358 if !ok { 359 return 360 } 361 for i, fb := range r.fallbacks { 362 if fb.Hash().Equals(pld.FallbackTransaction.Hash()) { 363 r.fallbacks = append(r.fallbacks[:i], r.fallbacks[i+1:]...) 364 break 365 } 366 } 367 if len(r.fallbacks) == 0 { 368 delete(n.requests, r.main.Hash()) 369 } 370 } 371 372 // PostPersist is a callback which is called after a new block event is received. 373 // PostPersist must not be called under the blockchain lock, because it uses finalization function. 374 func (n *Notary) PostPersist() { 375 if !n.started.Load() { 376 return 377 } 378 acc := n.getAccount() 379 if acc == nil { 380 return 381 } 382 383 n.reqMtx.Lock() 384 defer n.reqMtx.Unlock() 385 currHeight := n.Config.Chain.BlockHeight() 386 for h, r := range n.requests { 387 if !r.isSent && r.isMainCompleted() && r.minNotValidBefore > currHeight { 388 if err := n.finalize(acc, r.main, h); err != nil { 389 n.Config.Log.Error("failed to finalize main transaction", zap.Error(err)) 390 } 391 continue 392 } 393 if r.minNotValidBefore <= currHeight { // then at least one of the fallbacks can already be sent. 394 for _, fb := range r.fallbacks { 395 if nvb := fb.GetAttributes(transaction.NotValidBeforeT)[0].Value.(*transaction.NotValidBefore).Height; nvb <= currHeight { 396 // Ignore the error, wait for the next block to resend them 397 _ = n.finalize(acc, fb, h) 398 } 399 } 400 } 401 } 402 } 403 404 // finalize adds missing Notary witnesses to the transaction (main or fallback) and pushes it to the network. 405 func (n *Notary) finalize(acc *wallet.Account, tx *transaction.Transaction, h util.Uint256) error { 406 notaryWitness := transaction.Witness{ 407 InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, acc.SignHashable(n.Network, tx)...), 408 VerificationScript: []byte{}, 409 } 410 for i, signer := range tx.Signers { 411 if signer.Account == n.Config.Chain.GetNotaryContractScriptHash() { 412 tx.Scripts[i] = notaryWitness 413 break 414 } 415 } 416 newTx, err := updateTxSize(tx) 417 if err != nil { 418 return fmt.Errorf("failed to update completed transaction's size: %w", err) 419 } 420 421 n.pushNewTx(newTx, h) 422 423 return nil 424 } 425 426 type txHashPair struct { 427 tx *transaction.Transaction 428 mainHash util.Uint256 429 } 430 431 func (n *Notary) pushNewTx(tx *transaction.Transaction, h util.Uint256) { 432 select { 433 case n.newTxs <- txHashPair{tx, h}: 434 default: 435 } 436 } 437 438 func (n *Notary) newTxCallbackLoop() { 439 for { 440 select { 441 case tx := <-n.newTxs: 442 isMain := tx.tx.Hash() == tx.mainHash 443 444 n.reqMtx.Lock() 445 r, ok := n.requests[tx.mainHash] 446 if !ok || isMain && (r.isSent || r.minNotValidBefore <= n.Config.Chain.BlockHeight()) { 447 n.reqMtx.Unlock() 448 continue 449 } 450 if !isMain { 451 // Ensure that fallback was not already completed. 452 var isPending bool 453 for _, fb := range r.fallbacks { 454 if fb.Hash() == tx.tx.Hash() { 455 isPending = true 456 break 457 } 458 } 459 if !isPending { 460 n.reqMtx.Unlock() 461 continue 462 } 463 } 464 465 n.reqMtx.Unlock() 466 err := n.onTransaction(tx.tx) 467 if err != nil { 468 n.Config.Log.Error("new transaction callback finished with error", 469 zap.Error(err), 470 zap.Bool("is main", isMain)) 471 continue 472 } 473 474 n.reqMtx.Lock() 475 if isMain { 476 r.isSent = true 477 } else { 478 for i := range r.fallbacks { 479 if r.fallbacks[i].Hash() == tx.tx.Hash() { 480 r.fallbacks = append(r.fallbacks[:i], r.fallbacks[i+1:]...) 481 break 482 } 483 } 484 if len(r.fallbacks) == 0 { 485 delete(n.requests, tx.mainHash) 486 } 487 } 488 n.reqMtx.Unlock() 489 case <-n.stopCh: 490 return 491 } 492 } 493 } 494 495 // updateTxSize returns a transaction with re-calculated size and an error. 496 func updateTxSize(tx *transaction.Transaction) (*transaction.Transaction, error) { 497 bw := io.NewBufBinWriter() 498 tx.EncodeBinary(bw.BinWriter) 499 if bw.Err != nil { 500 return nil, fmt.Errorf("encode binary: %w", bw.Err) 501 } 502 return transaction.NewTransactionFromBytes(tx.Bytes()) 503 } 504 505 // verifyIncompleteWitnesses checks that the tx either doesn't have all witnesses attached (in this case none of them 506 // can be multisignature) or it only has a partial multisignature. It returns the request type (sig/multisig), the 507 // number of signatures to be collected, sorted public keys (for multisig request only) and an error. 508 func (n *Notary) verifyIncompleteWitnesses(tx *transaction.Transaction, nKeysExpected uint8) ([]witnessInfo, error) { 509 var nKeysActual uint8 510 if len(tx.Signers) < 2 { 511 return nil, errors.New("transaction should have at least 2 signers") 512 } 513 if !tx.HasSigner(n.Config.Chain.GetNotaryContractScriptHash()) { 514 return nil, fmt.Errorf("P2PNotary contract should be a signer of the transaction") 515 } 516 result := make([]witnessInfo, len(tx.Signers)) 517 for i, w := range tx.Scripts { 518 // Do not check witness for a Notary contract -- it will be replaced by proper witness in any case. 519 // Also, do not check other contract-based witnesses (they can be combined with anything) 520 if len(w.VerificationScript) == 0 { 521 result[i] = witnessInfo{ 522 typ: Contract, 523 nSigsLeft: 0, 524 } 525 continue 526 } 527 if !tx.Signers[i].Account.Equals(hash.Hash160(w.VerificationScript)) { // https://github.com/nspcc-dev/neo-go/pull/1658#discussion_r564265987 528 return nil, fmt.Errorf("transaction should have valid verification script for signer #%d", i) 529 } 530 // Each verification script is allowed to have either one signature or zero signatures. If signature is provided, then need to verify it. 531 if len(w.InvocationScript) != 0 { 532 if len(w.InvocationScript) != 66 || !bytes.HasPrefix(w.InvocationScript, []byte{byte(opcode.PUSHDATA1), keys.SignatureLen}) { 533 return nil, fmt.Errorf("witness #%d: invocation script should have length = 66 and be of the form [PUSHDATA1, 64, signatureBytes...]", i) 534 } 535 } 536 if nSigs, pubsBytes, ok := vm.ParseMultiSigContract(w.VerificationScript); ok { 537 result[i] = witnessInfo{ 538 typ: MultiSignature, 539 nSigsLeft: uint8(nSigs), 540 pubs: make(keys.PublicKeys, len(pubsBytes)), 541 } 542 for j, pBytes := range pubsBytes { 543 pub, err := keys.NewPublicKeyFromBytes(pBytes, elliptic.P256()) 544 if err != nil { 545 return nil, fmt.Errorf("witness #%d: invalid bytes of #%d public key: %s", i, j, hex.EncodeToString(pBytes)) 546 } 547 result[i].pubs[j] = pub 548 } 549 nKeysActual += uint8(len(pubsBytes)) 550 continue 551 } 552 if pBytes, ok := vm.ParseSignatureContract(w.VerificationScript); ok { 553 pub, err := keys.NewPublicKeyFromBytes(pBytes, elliptic.P256()) 554 if err != nil { 555 return nil, fmt.Errorf("witness #%d: invalid bytes of public key: %s", i, hex.EncodeToString(pBytes)) 556 } 557 result[i] = witnessInfo{ 558 typ: Signature, 559 nSigsLeft: 1, 560 pubs: keys.PublicKeys{pub}, 561 } 562 nKeysActual++ 563 continue 564 } 565 return nil, fmt.Errorf("witness #%d: unable to detect witness type, only sig/multisig/contract are supported", i) 566 } 567 if nKeysActual != nKeysExpected { 568 return nil, fmt.Errorf("expected and actual NKeys mismatch: %d vs %d", nKeysExpected, nKeysActual) 569 } 570 return result, nil 571 }