mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2025-02-17 05:47:38 +00:00
Maintain the number of signatures left to collect instead of maintaining the number of already collected signatures and overall number of signatures.
477 lines
15 KiB
Go
477 lines
15 KiB
Go
package notary
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/elliptic"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/mempoolevent"
|
|
"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/io"
|
|
"github.com/nspcc-dev/neo-go/pkg/network/payload"
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type (
|
|
// Notary represents Notary module.
|
|
Notary struct {
|
|
Config Config
|
|
|
|
Network netmode.Magic
|
|
|
|
// onTransaction is a callback for completed transactions (mains or fallbacks) sending.
|
|
onTransaction func(tx *transaction.Transaction) error
|
|
// newTxs is a channel where new transactions are sent
|
|
// to be processed in a `onTransaction` callback.
|
|
newTxs chan txHashPair
|
|
|
|
// reqMtx protects requests list.
|
|
reqMtx sync.RWMutex
|
|
// requests represents the map of main transactions which needs to be completed
|
|
// with the associated fallback transactions grouped by the main transaction hash
|
|
requests map[util.Uint256]*request
|
|
|
|
// accMtx protects account.
|
|
accMtx sync.RWMutex
|
|
currAccount *wallet.Account
|
|
wallet *wallet.Wallet
|
|
|
|
mp *mempool.Pool
|
|
// requests channel
|
|
reqCh chan mempoolevent.Event
|
|
blocksCh chan *block.Block
|
|
stopCh chan struct{}
|
|
}
|
|
|
|
// Config represents external configuration for Notary module.
|
|
Config struct {
|
|
MainCfg config.P2PNotary
|
|
Chain blockchainer.Blockchainer
|
|
Log *zap.Logger
|
|
}
|
|
)
|
|
|
|
const defaultTxChannelCapacity = 100
|
|
|
|
// request represents Notary service request.
|
|
type request struct {
|
|
typ RequestType
|
|
// isSent indicates whether main transaction was successfully sent to the network.
|
|
isSent bool
|
|
main *transaction.Transaction
|
|
// minNotValidBefore is the minimum NVB value among fallbacks transactions.
|
|
// We stop trying to send mainTx to the network if the chain reaches minNotValidBefore height.
|
|
minNotValidBefore uint32
|
|
fallbacks []*transaction.Transaction
|
|
// nSigsLeft is the number of signatures left to collect to complete main transaction.
|
|
// Initial nSigsLeft value is defined as following:
|
|
// nSigsLeft == nKeys for standard signature request;
|
|
// nSigsLeft <= nKeys for multisignature request;
|
|
// nSigsLeft == 0 when all received requests were invalid, so check request.typ before access to nSigs.
|
|
nSigsLeft uint8
|
|
|
|
// sigs is a map of partial multisig invocation scripts [opcode.PUSHDATA1+64+signatureBytes] grouped by public keys
|
|
sigs map[*keys.PublicKey][]byte
|
|
}
|
|
|
|
// NewNotary returns new Notary module.
|
|
func NewNotary(cfg Config, net netmode.Magic, mp *mempool.Pool, onTransaction func(tx *transaction.Transaction) error) (*Notary, error) {
|
|
w := cfg.MainCfg.UnlockWallet
|
|
wallet, err := wallet.NewWalletFromFile(w.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
haveAccount := false
|
|
for _, acc := range wallet.Accounts {
|
|
if err := acc.Decrypt(w.Password, wallet.Scrypt); err == nil {
|
|
haveAccount = true
|
|
break
|
|
}
|
|
}
|
|
if !haveAccount {
|
|
return nil, errors.New("no wallet account could be unlocked")
|
|
}
|
|
|
|
return &Notary{
|
|
requests: make(map[util.Uint256]*request),
|
|
Config: cfg,
|
|
Network: net,
|
|
wallet: wallet,
|
|
onTransaction: onTransaction,
|
|
newTxs: make(chan txHashPair, defaultTxChannelCapacity),
|
|
mp: mp,
|
|
reqCh: make(chan mempoolevent.Event),
|
|
blocksCh: make(chan *block.Block),
|
|
stopCh: make(chan struct{}),
|
|
}, nil
|
|
}
|
|
|
|
// Run runs Notary module and should be called in a separate goroutine.
|
|
func (n *Notary) Run() {
|
|
n.Config.Log.Info("starting notary service")
|
|
n.Config.Chain.SubscribeForBlocks(n.blocksCh)
|
|
n.mp.SubscribeForTransactions(n.reqCh)
|
|
go n.newTxCallbackLoop()
|
|
for {
|
|
select {
|
|
case <-n.stopCh:
|
|
n.mp.UnsubscribeFromTransactions(n.reqCh)
|
|
n.Config.Chain.UnsubscribeFromBlocks(n.blocksCh)
|
|
return
|
|
case event := <-n.reqCh:
|
|
if req, ok := event.Data.(*payload.P2PNotaryRequest); ok {
|
|
switch event.Type {
|
|
case mempoolevent.TransactionAdded:
|
|
n.OnNewRequest(req)
|
|
case mempoolevent.TransactionRemoved:
|
|
n.OnRequestRemoval(req)
|
|
}
|
|
}
|
|
case <-n.blocksCh:
|
|
// new block was added, need to check for valid fallbacks
|
|
n.PostPersist()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop shutdowns Notary module.
|
|
func (n *Notary) Stop() {
|
|
close(n.stopCh)
|
|
}
|
|
|
|
// OnNewRequest is a callback method which is called after new notary request is added to the notary request pool.
|
|
func (n *Notary) OnNewRequest(payload *payload.P2PNotaryRequest) {
|
|
acc := n.getAccount()
|
|
if acc == nil {
|
|
return
|
|
}
|
|
|
|
nvbFallback := payload.FallbackTransaction.GetAttributes(transaction.NotValidBeforeT)[0].Value.(*transaction.NotValidBefore).Height
|
|
nKeys := payload.MainTransaction.GetAttributes(transaction.NotaryAssistedT)[0].Value.(*transaction.NotaryAssisted).NKeys
|
|
typ, nSigs, pubs, validationErr := n.verifyIncompleteWitnesses(payload.MainTransaction, nKeys)
|
|
n.reqMtx.Lock()
|
|
defer n.reqMtx.Unlock()
|
|
r, exists := n.requests[payload.MainTransaction.Hash()]
|
|
if exists {
|
|
for _, fb := range r.fallbacks {
|
|
if fb.Hash().Equals(payload.FallbackTransaction.Hash()) {
|
|
return // then we already have processed this request
|
|
}
|
|
}
|
|
if nvbFallback < r.minNotValidBefore {
|
|
r.minNotValidBefore = nvbFallback
|
|
}
|
|
if r.typ == Unknown && validationErr == nil {
|
|
r.typ = typ
|
|
r.nSigsLeft = nSigs
|
|
}
|
|
} else {
|
|
r = &request{
|
|
nSigsLeft: nSigs,
|
|
main: payload.MainTransaction,
|
|
typ: typ,
|
|
minNotValidBefore: nvbFallback,
|
|
}
|
|
n.requests[payload.MainTransaction.Hash()] = r
|
|
}
|
|
r.fallbacks = append(r.fallbacks, payload.FallbackTransaction)
|
|
if exists && r.typ != Unknown && r.nSigsLeft == 0 { // already collected sufficient number of signatures to complete main transaction
|
|
return
|
|
}
|
|
if validationErr == nil {
|
|
loop:
|
|
for i, w := range payload.MainTransaction.Scripts {
|
|
if payload.MainTransaction.Signers[i].Account.Equals(n.Config.Chain.GetNotaryContractScriptHash()) {
|
|
continue
|
|
}
|
|
if len(w.InvocationScript) != 0 && len(w.VerificationScript) != 0 {
|
|
switch r.typ {
|
|
case Signature:
|
|
if !exists {
|
|
r.nSigsLeft--
|
|
} else if len(r.main.Scripts[i].InvocationScript) == 0 { // need this check because signature can already be added (consider receiving the same payload multiple times)
|
|
r.main.Scripts[i] = w
|
|
r.nSigsLeft--
|
|
}
|
|
if r.nSigsLeft == 0 {
|
|
break loop
|
|
}
|
|
case MultiSignature:
|
|
if r.sigs == nil {
|
|
r.sigs = make(map[*keys.PublicKey][]byte)
|
|
}
|
|
|
|
hash := hash.NetSha256(uint32(n.Network), r.main).BytesBE()
|
|
for _, pub := range pubs {
|
|
if r.sigs[pub] != nil {
|
|
continue // signature for this pub has already been added
|
|
}
|
|
if pub.Verify(w.InvocationScript[2:], hash) { // then pub is the owner of the signature
|
|
r.sigs[pub] = w.InvocationScript
|
|
r.nSigsLeft--
|
|
if r.nSigsLeft == 0 {
|
|
var invScript []byte
|
|
for j := range pubs {
|
|
if sig, ok := r.sigs[pubs[j]]; ok {
|
|
invScript = append(invScript, sig...)
|
|
}
|
|
}
|
|
r.main.Scripts[i].InvocationScript = invScript
|
|
}
|
|
break loop
|
|
}
|
|
}
|
|
// pubKey was not found for the signature i.e. signature is bad - we're OK with that, let the fallback TX to be added
|
|
break loop // only one multisignature is allowed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if r.typ != Unknown && r.nSigsLeft == 0 && r.minNotValidBefore > n.Config.Chain.BlockHeight() {
|
|
if err := n.finalize(acc, r.main, payload.MainTransaction.Hash()); err != nil {
|
|
n.Config.Log.Error("failed to finalize main transaction", zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
// OnRequestRemoval is a callback which is called after fallback transaction is removed
|
|
// from the notary payload pool due to expiration, main tx appliance or any other reason.
|
|
func (n *Notary) OnRequestRemoval(pld *payload.P2PNotaryRequest) {
|
|
if n.getAccount() == nil {
|
|
return
|
|
}
|
|
|
|
n.reqMtx.Lock()
|
|
defer n.reqMtx.Unlock()
|
|
r, ok := n.requests[pld.MainTransaction.Hash()]
|
|
if !ok {
|
|
return
|
|
}
|
|
for i, fb := range r.fallbacks {
|
|
if fb.Hash().Equals(pld.FallbackTransaction.Hash()) {
|
|
r.fallbacks = append(r.fallbacks[:i], r.fallbacks[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
if len(r.fallbacks) == 0 {
|
|
delete(n.requests, r.main.Hash())
|
|
}
|
|
}
|
|
|
|
// PostPersist is a callback which is called after new block event is received.
|
|
// PostPersist must not be called under the blockchain lock, because it uses finalization function.
|
|
func (n *Notary) PostPersist() {
|
|
acc := n.getAccount()
|
|
if acc == nil {
|
|
return
|
|
}
|
|
|
|
n.reqMtx.Lock()
|
|
defer n.reqMtx.Unlock()
|
|
currHeight := n.Config.Chain.BlockHeight()
|
|
for h, r := range n.requests {
|
|
if !r.isSent && r.typ != Unknown && r.nSigsLeft == 0 && r.minNotValidBefore > currHeight {
|
|
if err := n.finalize(acc, r.main, h); err != nil {
|
|
n.Config.Log.Error("failed to finalize main transaction", zap.Error(err))
|
|
}
|
|
continue
|
|
}
|
|
if r.minNotValidBefore <= currHeight { // then at least one of the fallbacks can already be sent.
|
|
for _, fb := range r.fallbacks {
|
|
if nvb := fb.GetAttributes(transaction.NotValidBeforeT)[0].Value.(*transaction.NotValidBefore).Height; nvb <= currHeight {
|
|
// Ignore the error, wait for the next block to resend them
|
|
_ = n.finalize(acc, fb, h)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// finalize adds missing Notary witnesses to the transaction (main or fallback) and pushes it to the network.
|
|
func (n *Notary) finalize(acc *wallet.Account, tx *transaction.Transaction, h util.Uint256) error {
|
|
notaryWitness := transaction.Witness{
|
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc.PrivateKey().SignHashable(uint32(n.Network), tx)...),
|
|
VerificationScript: []byte{},
|
|
}
|
|
for i, signer := range tx.Signers {
|
|
if signer.Account == n.Config.Chain.GetNotaryContractScriptHash() {
|
|
tx.Scripts[i] = notaryWitness
|
|
break
|
|
}
|
|
}
|
|
newTx, err := updateTxSize(tx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update completed transaction's size: %w", err)
|
|
}
|
|
|
|
n.pushNewTx(newTx, h)
|
|
|
|
return nil
|
|
}
|
|
|
|
type txHashPair struct {
|
|
tx *transaction.Transaction
|
|
mainHash util.Uint256
|
|
}
|
|
|
|
func (n *Notary) pushNewTx(tx *transaction.Transaction, h util.Uint256) {
|
|
select {
|
|
case n.newTxs <- txHashPair{tx, h}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (n *Notary) newTxCallbackLoop() {
|
|
for {
|
|
select {
|
|
case tx := <-n.newTxs:
|
|
isMain := tx.tx.Hash() == tx.mainHash
|
|
|
|
n.reqMtx.Lock()
|
|
r, ok := n.requests[tx.mainHash]
|
|
if !ok || isMain && (r.isSent || r.minNotValidBefore <= n.Config.Chain.BlockHeight()) {
|
|
n.reqMtx.Unlock()
|
|
continue
|
|
}
|
|
if !isMain {
|
|
// Ensure that fallback was not already completed.
|
|
var isPending bool
|
|
for _, fb := range r.fallbacks {
|
|
if fb.Hash() == tx.tx.Hash() {
|
|
isPending = true
|
|
break
|
|
}
|
|
}
|
|
if !isPending {
|
|
n.reqMtx.Unlock()
|
|
continue
|
|
}
|
|
}
|
|
|
|
n.reqMtx.Unlock()
|
|
err := n.onTransaction(tx.tx)
|
|
if err != nil {
|
|
n.Config.Log.Error("new transaction callback finished with error", zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
n.reqMtx.Lock()
|
|
if isMain {
|
|
r.isSent = true
|
|
} else {
|
|
for i := range r.fallbacks {
|
|
if r.fallbacks[i].Hash() == tx.tx.Hash() {
|
|
r.fallbacks = append(r.fallbacks[:i], r.fallbacks[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
if len(r.fallbacks) == 0 {
|
|
delete(n.requests, tx.mainHash)
|
|
}
|
|
}
|
|
n.reqMtx.Unlock()
|
|
case <-n.stopCh:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// updateTxSize returns transaction with re-calculated size and an error.
|
|
func updateTxSize(tx *transaction.Transaction) (*transaction.Transaction, error) {
|
|
bw := io.NewBufBinWriter()
|
|
tx.EncodeBinary(bw.BinWriter)
|
|
if bw.Err != nil {
|
|
return nil, fmt.Errorf("encode binary: %w", bw.Err)
|
|
}
|
|
return transaction.NewTransactionFromBytes(tx.Bytes())
|
|
}
|
|
|
|
// verifyIncompleteWitnesses checks that tx either doesn't have all witnesses attached (in this case none of them
|
|
// can be multisignature), or it only has a partial multisignature. It returns the request type (sig/multisig), the
|
|
// number of signatures to be collected, sorted public keys (for multisig request only) and an error.
|
|
func (n *Notary) verifyIncompleteWitnesses(tx *transaction.Transaction, nKeys uint8) (RequestType, uint8, keys.PublicKeys, error) {
|
|
var (
|
|
typ RequestType
|
|
nSigs int
|
|
nKeysActual uint8
|
|
pubsBytes [][]byte
|
|
pubs keys.PublicKeys
|
|
ok bool
|
|
)
|
|
if len(tx.Signers) < 2 {
|
|
return Unknown, 0, nil, errors.New("transaction should have at least 2 signers")
|
|
}
|
|
if !tx.HasSigner(n.Config.Chain.GetNotaryContractScriptHash()) {
|
|
return Unknown, 0, nil, fmt.Errorf("P2PNotary contract should be a signer of the transaction")
|
|
}
|
|
|
|
for i, w := range tx.Scripts {
|
|
// do not check witness for Notary contract -- it will be replaced by proper witness in any case.
|
|
if tx.Signers[i].Account == n.Config.Chain.GetNotaryContractScriptHash() {
|
|
continue
|
|
}
|
|
if len(w.VerificationScript) == 0 {
|
|
// then it's a contract verification (can be combined with anything)
|
|
continue
|
|
}
|
|
if !tx.Signers[i].Account.Equals(hash.Hash160(w.VerificationScript)) { // https://github.com/nspcc-dev/neo-go/pull/1658#discussion_r564265987
|
|
return Unknown, 0, nil, fmt.Errorf("transaction should have valid verification script for signer #%d", i)
|
|
}
|
|
if nSigs, pubsBytes, ok = vm.ParseMultiSigContract(w.VerificationScript); ok {
|
|
if typ == Signature || typ == MultiSignature {
|
|
return Unknown, 0, nil, fmt.Errorf("bad type of witness #%d: only one multisignature witness is allowed", i)
|
|
}
|
|
typ = MultiSignature
|
|
nKeysActual = uint8(len(pubsBytes))
|
|
if len(w.InvocationScript) != 66 || !bytes.HasPrefix(w.InvocationScript, []byte{byte(opcode.PUSHDATA1), 64}) {
|
|
return Unknown, 0, nil, fmt.Errorf("multisignature invocation script should have length = 66 and be of the form [PUSHDATA1, 64, signatureBytes...]")
|
|
}
|
|
continue
|
|
}
|
|
if vm.IsSignatureContract(w.VerificationScript) {
|
|
if typ == MultiSignature {
|
|
return Unknown, 0, nil, fmt.Errorf("bad type of witness #%d: multisignature witness can not be combined with other witnesses", i)
|
|
}
|
|
typ = Signature
|
|
nSigs = int(nKeys)
|
|
continue
|
|
}
|
|
return Unknown, 0, nil, fmt.Errorf("unable to define the type of witness #%d", i)
|
|
}
|
|
switch typ {
|
|
case Signature:
|
|
if len(tx.Scripts) < int(nKeys+1) {
|
|
return Unknown, 0, nil, fmt.Errorf("transaction should comtain at least %d witnesses (1 for notary + nKeys)", nKeys+1)
|
|
}
|
|
case MultiSignature:
|
|
if nKeysActual != nKeys {
|
|
return Unknown, 0, nil, fmt.Errorf("bad m out of n partial multisignature witness: expected n = %d, got n = %d", nKeys, nKeysActual)
|
|
}
|
|
pubs = make(keys.PublicKeys, len(pubsBytes))
|
|
for i, pBytes := range pubsBytes {
|
|
pub, err := keys.NewPublicKeyFromBytes(pBytes, elliptic.P256())
|
|
if err != nil {
|
|
return Unknown, 0, nil, fmt.Errorf("invalid bytes of #%d public key: %s", i, hex.EncodeToString(pBytes))
|
|
}
|
|
pubs[i] = pub
|
|
}
|
|
default:
|
|
return Unknown, 0, nil, errors.New("unexpected Notary request type")
|
|
}
|
|
return typ, uint8(nSigs), pubs, nil
|
|
}
|