2020-12-30 08:01:13 +00:00
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/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/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/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
// onTransaction is a callback for completed transactions (mains or fallbacks) sending.
onTransaction func ( tx * transaction . Transaction ) error
// 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
2021-01-15 12:40:15 +00:00
mp * mempool . Pool
// requests channel
reqCh chan mempool . Event
stopCh chan struct { }
2020-12-30 08:01:13 +00:00
}
// Config represents external configuration for Notary module.
Config struct {
MainCfg config . P2PNotary
Chain blockchainer . Blockchainer
Log * zap . Logger
}
)
// 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
// nSigs is the number of signatures to be collected.
// nSigs == nKeys for standard signature request;
// nSigs <= nKeys for multisignature request.
// nSigs is 0 when all received requests were invalid, so check request.typ before access to nSigs.
nSigs uint8
// nSigsCollected is the number of already collected signatures
nSigsCollected 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.
2021-01-15 12:40:15 +00:00
func NewNotary ( bc blockchainer . Blockchainer , mp * mempool . Pool , log * zap . Logger , onTransaction func ( tx * transaction . Transaction ) error ) ( * Notary , error ) {
2020-12-30 08:01:13 +00:00
cfg := bc . GetConfig ( ) . P2PNotary
w := cfg . 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 ) ; 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 : Config {
MainCfg : cfg ,
Chain : bc ,
Log : log ,
} ,
wallet : wallet ,
onTransaction : onTransaction ,
2021-01-15 12:40:15 +00:00
mp : mp ,
reqCh : make ( chan mempool . Event ) ,
stopCh : make ( chan struct { } ) ,
2020-12-30 08:01:13 +00:00
} , nil
}
2021-01-15 12:40:15 +00:00
// Run runs Notary module and should be called in a separate goroutine.
func ( n * Notary ) Run ( ) {
n . mp . SubscribeForTransactions ( n . reqCh )
for {
select {
case <- n . stopCh :
n . mp . UnsubscribeFromTransactions ( n . reqCh )
return
case event := <- n . reqCh :
if req , ok := event . Data . ( * payload . P2PNotaryRequest ) ; ok {
switch event . Type {
case mempool . TransactionAdded :
n . OnNewRequest ( req )
case mempool . TransactionRemoved :
n . OnRequestRemoval ( req )
}
}
}
}
}
// Stop shutdowns Notary module.
func ( n * Notary ) Stop ( ) {
close ( n . stopCh )
}
2020-12-30 08:01:13 +00:00
// 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 ) {
if n . getAccount ( ) == 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 . nSigs = nSigs
}
} else {
r = & request {
nSigs : 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 . nSigsCollected >= r . nSigs { // 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 . nSigsCollected ++
} 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 . nSigsCollected ++
}
if r . nSigsCollected == r . nSigs {
break loop
}
case MultiSignature :
if r . sigs == nil {
r . sigs = make ( map [ * keys . PublicKey ] [ ] byte )
}
hash := r . main . GetSignedHash ( ) . 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 . nSigsCollected ++
if r . nSigsCollected == r . nSigs {
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 . nSigsCollected == nSigs && r . minNotValidBefore > n . Config . Chain . BlockHeight ( ) {
if err := n . finalize ( r . main ) ; err != nil {
n . Config . Log . Error ( "failed to finalize main transaction" , zap . Error ( err ) )
} else {
r . isSent = true
}
}
}
// 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 is persisted.
func ( n * Notary ) PostPersist ( bc blockchainer . Blockchainer , pool * mempool . Pool , b * block . Block ) {
if n . getAccount ( ) == nil {
return
}
n . reqMtx . Lock ( )
defer n . reqMtx . Unlock ( )
for h , r := range n . requests {
if ! r . isSent && r . typ != Unknown && r . nSigs == r . nSigsCollected && r . minNotValidBefore > bc . BlockHeight ( ) {
if err := n . finalize ( r . main ) ; err != nil {
n . Config . Log . Error ( "failed to finalize main transaction" , zap . Error ( err ) )
} else {
r . isSent = true
}
continue
}
if r . minNotValidBefore <= bc . BlockHeight ( ) { // then at least one of the fallbacks can already be sent.
newFallbacks := r . fallbacks [ : 0 ]
for _ , fb := range r . fallbacks {
if nvb := fb . GetAttributes ( transaction . NotValidBeforeT ) [ 0 ] . Value . ( * transaction . NotValidBefore ) . Height ; nvb <= bc . BlockHeight ( ) {
if err := n . finalize ( fb ) ; err != nil {
newFallbacks = append ( newFallbacks , fb ) // wait for the next block to resend them
}
} else {
newFallbacks = append ( newFallbacks , fb )
}
}
if len ( newFallbacks ) == 0 {
delete ( n . requests , h )
} else {
r . fallbacks = newFallbacks
}
}
}
}
// finalize adds missing Notary witnesses to the transaction (main or fallback) and pushes it to the network.
func ( n * Notary ) finalize ( tx * transaction . Transaction ) error {
acc := n . getAccount ( )
if acc == nil {
2021-01-26 16:37:19 +00:00
panic ( errors . New ( "no available Notary account" ) ) // unreachable code, because all callers of `finalize` check that acc != nil
2020-12-30 08:01:13 +00:00
}
notaryWitness := transaction . Witness {
InvocationScript : append ( [ ] byte { byte ( opcode . PUSHDATA1 ) , 64 } , acc . PrivateKey ( ) . Sign ( tx . GetSignedPart ( ) ) ... ) ,
VerificationScript : [ ] byte { } ,
}
for i , signer := range tx . Signers {
if signer . Account == n . Config . Chain . GetNotaryContractScriptHash ( ) {
tx . Scripts [ i ] = notaryWitness
break
}
}
return n . onTransaction ( tx )
}
// 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 len ( tx . Signers ) != len ( tx . Scripts ) {
return Unknown , 0 , nil , fmt . Errorf ( "transaction should have %d witnesses attached (completed + dummy)" , len ( tx . 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
}