forked from TrueCloudLab/frostfs-node
416 lines
12 KiB
Go
416 lines
12 KiB
Go
package event
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
|
"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/smartcontract"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
"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"
|
|
)
|
|
|
|
var (
|
|
errNotContractCall = errors.New("received main tx is not a contract call")
|
|
errUnexpectedWitnessAmount = errors.New("received main tx has unexpected amount of witnesses")
|
|
errUnexpectedCosignersAmount = errors.New("received main tx has unexpected amount of cosigners")
|
|
errIncorrectAlphabetSigner = errors.New("received main tx has incorrect Alphabet signer")
|
|
errIncorrectProxyWitnesses = errors.New("received main tx has non-empty Proxy witnesses")
|
|
errIncorrectInvokerWitnesses = errors.New("received main tx has empty Invoker witness")
|
|
errIncorrectAlphabet = errors.New("received main tx has incorrect Alphabet verification")
|
|
errIncorrectNotaryPlaceholder = errors.New("received main tx has incorrect Notary contract placeholder")
|
|
errIncorrectAttributesAmount = errors.New("received main tx has incorrect attributes amount")
|
|
errIncorrectAttribute = errors.New("received main tx has incorrect attribute")
|
|
errIncorrectCallFlag = errors.New("received main tx has unexpected call flag")
|
|
errIncorrectArgPacking = errors.New("received main tx has incorrect argument packing")
|
|
errUnexpectedCONVERT = errors.New("received main tx has unexpected CONVERT opcode")
|
|
|
|
errIncorrectFBAttributesAmount = errors.New("received fallback tx has incorrect attributes amount")
|
|
errIncorrectFBAttributes = errors.New("received fallback tx has incorrect attributes")
|
|
|
|
// ErrTXAlreadyHandled is returned if received TX has already been signed.
|
|
ErrTXAlreadyHandled = errors.New("received main tx has already been handled")
|
|
)
|
|
|
|
// ExpiredTXError is returned if received fallback TX is already valid.
|
|
type ExpiredTXError struct {
|
|
CurrentBlockHeight uint32
|
|
FallbackTXNotValidBeforeHeight uint32
|
|
}
|
|
|
|
func (e *ExpiredTXError) Error() string {
|
|
return "received main tx has expired"
|
|
}
|
|
|
|
// BlockCounter must return block count of the network
|
|
// from which notary requests are received.
|
|
type BlockCounter interface {
|
|
BlockCount() (res uint32, err error)
|
|
}
|
|
|
|
// PreparatorPrm groups the required parameters of the Preparator constructor.
|
|
type PreparatorPrm struct {
|
|
AlphaKeys client.AlphabetKeys
|
|
|
|
// BlockCount must return block count of the network
|
|
// from which notary requests are received.
|
|
BlockCounter BlockCounter
|
|
}
|
|
|
|
// Preparator implements NotaryPreparator interface.
|
|
type Preparator struct {
|
|
// contractSysCall contract call in NeoVM
|
|
contractSysCall []byte
|
|
// dummyInvocationScript is invocation script from TX that is not signed.
|
|
dummyInvocationScript []byte
|
|
|
|
alphaKeys client.AlphabetKeys
|
|
|
|
blockCounter BlockCounter
|
|
}
|
|
|
|
// notaryPreparator inits and returns NotaryPreparator.
|
|
//
|
|
// Considered to be used for preparing notary request
|
|
// for parsing it by event.Listener.
|
|
func notaryPreparator(prm PreparatorPrm) NotaryPreparator {
|
|
switch {
|
|
case prm.AlphaKeys == nil:
|
|
panic("alphabet keys source must not be nil")
|
|
case prm.BlockCounter == nil:
|
|
panic("block counter must not be nil")
|
|
}
|
|
|
|
contractSysCall := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(contractSysCall, interopnames.ToID([]byte(interopnames.SystemContractCall)))
|
|
|
|
dummyInvocationScript := append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...)
|
|
|
|
return Preparator{
|
|
contractSysCall: contractSysCall,
|
|
dummyInvocationScript: dummyInvocationScript,
|
|
alphaKeys: prm.AlphaKeys,
|
|
blockCounter: prm.BlockCounter,
|
|
}
|
|
}
|
|
|
|
// Prepare converts raw notary requests to NotaryEvent.
|
|
//
|
|
// Returns ErrTXAlreadyHandled if transaction shouldn't be
|
|
// parsed and handled. It is not "error case". Every handled
|
|
// transaction is expected to be received one more time
|
|
// from the Notary service but already signed. This happens
|
|
// since every notary call is a new notary request in fact.
|
|
func (p Preparator) Prepare(nr *payload.P2PNotaryRequest) (NotaryEvent, error) {
|
|
err := p.validateNotaryRequest(nr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
opCode opcode.Opcode
|
|
param []byte
|
|
)
|
|
|
|
ctx := vm.NewContext(nr.MainTransaction.Script)
|
|
ops := make([]Op, 0, 10) // 10 is maximum num of opcodes for calling contracts with 4 args(no arrays of arrays)
|
|
|
|
for {
|
|
opCode, param, err = ctx.Next()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get next opcode in script: %w", err)
|
|
}
|
|
|
|
if opCode == opcode.RET {
|
|
break
|
|
}
|
|
|
|
ops = append(ops, Op{code: opCode, param: param})
|
|
}
|
|
|
|
opsLen := len(ops)
|
|
|
|
// check if it is tx with contract call
|
|
if !bytes.Equal(ops[opsLen-1].param, p.contractSysCall) {
|
|
return nil, errNotContractCall
|
|
}
|
|
|
|
// retrieve contract's script hash
|
|
contractHash, err := util.Uint160DecodeBytesBE(ops[opsLen-2].param)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode contract hash: %w", err)
|
|
}
|
|
|
|
// retrieve contract's method
|
|
contractMethod := string(ops[opsLen-3].param)
|
|
|
|
// check if there is a call flag(must be in range [0:15))
|
|
callFlag := callflag.CallFlag(ops[opsLen-4].code - opcode.PUSH0)
|
|
if callFlag > callflag.All {
|
|
return nil, errIncorrectCallFlag
|
|
}
|
|
|
|
args := ops[:opsLen-4]
|
|
|
|
if len(args) != 0 {
|
|
err = p.validateParameterOpcodes(args)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("validate arguments: %w", err)
|
|
}
|
|
|
|
// without args packing opcodes
|
|
args = args[:len(args)-2]
|
|
}
|
|
|
|
return parsedNotaryEvent{
|
|
hash: contractHash,
|
|
notaryType: NotaryTypeFromString(contractMethod),
|
|
params: args,
|
|
raw: nr,
|
|
}, nil
|
|
}
|
|
|
|
func (p Preparator) validateNotaryRequest(nr *payload.P2PNotaryRequest) error {
|
|
// notary request's main tx is expected to have
|
|
// three or four witnesses: one for proxy contract,
|
|
// one for alphabet multisignature, one optional for
|
|
// notary's invoker and one is for notary contract
|
|
ln := len(nr.MainTransaction.Scripts)
|
|
switch ln {
|
|
case 3, 4:
|
|
default:
|
|
return errUnexpectedWitnessAmount
|
|
}
|
|
invokerWitness := ln == 4
|
|
|
|
// alphabet node should handle only notary requests that do not yet have inner
|
|
// ring multisignature filled => such main TXs either have empty invocation script
|
|
// of the inner ring witness (in case if Notary Actor is used to create request)
|
|
// or have it filled with dummy bytes (if request was created manually with the old
|
|
// neo-go API)
|
|
//
|
|
// this check prevents notary flow recursion
|
|
if !(len(nr.MainTransaction.Scripts[1].InvocationScript) == 0 ||
|
|
bytes.Equal(nr.MainTransaction.Scripts[1].InvocationScript, p.dummyInvocationScript)) { // compatibility with old version
|
|
return ErrTXAlreadyHandled
|
|
}
|
|
|
|
currentAlphabet, err := p.alphaKeys()
|
|
if err != nil {
|
|
return fmt.Errorf("fetch Alphabet public keys: %w", err)
|
|
}
|
|
|
|
err = p.validateCosigners(ln, nr.MainTransaction.Signers, currentAlphabet)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// validate main TX's notary attribute
|
|
err = p.validateAttributes(nr.MainTransaction.Attributes, currentAlphabet, invokerWitness)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// validate main TX's witnesses
|
|
err = p.validateWitnesses(nr.MainTransaction.Scripts, currentAlphabet, invokerWitness)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// validate main TX expiration
|
|
return p.validateExpiration(nr.FallbackTransaction)
|
|
}
|
|
|
|
func (p Preparator) validateParameterOpcodes(ops []Op) error {
|
|
l := len(ops)
|
|
|
|
if ops[l-1].code != opcode.PACK {
|
|
return fmt.Errorf("unexpected packing opcode: %s", ops[l-1].code)
|
|
}
|
|
|
|
argsLen, err := IntFromOpcode(ops[l-2])
|
|
if err != nil {
|
|
return fmt.Errorf("parse argument len: %w", err)
|
|
}
|
|
|
|
err = validateNestedArgs(argsLen, ops[:l-2])
|
|
return err
|
|
}
|
|
|
|
func validateNestedArgs(expArgLen int64, ops []Op) error {
|
|
var (
|
|
currentCode opcode.Opcode
|
|
|
|
opsLenGot = len(ops)
|
|
)
|
|
|
|
for i := opsLenGot - 1; i >= 0; i-- {
|
|
// only PUSH(also, PACK for arrays and CONVERT for booleans)
|
|
// codes are allowed; number of params and their content must
|
|
// be checked in a notary parser and a notary handler of a
|
|
// particular contract
|
|
switch currentCode = ops[i].code; {
|
|
case currentCode <= opcode.PUSH16:
|
|
case currentCode == opcode.CONVERT:
|
|
if i == 0 || ops[i-1].code != opcode.PUSHT && ops[i-1].code != opcode.PUSHF {
|
|
return errUnexpectedCONVERT
|
|
}
|
|
|
|
expArgLen++
|
|
case currentCode == opcode.PACK:
|
|
if i == 0 {
|
|
return errIncorrectArgPacking
|
|
}
|
|
|
|
argsLen, err := IntFromOpcode(ops[i-1])
|
|
if err != nil {
|
|
return fmt.Errorf("parse argument len: %w", err)
|
|
}
|
|
|
|
expArgLen += argsLen + 1
|
|
i--
|
|
default:
|
|
return fmt.Errorf("received main tx has unexpected(not PUSH) NeoVM opcode: %s", currentCode)
|
|
}
|
|
}
|
|
|
|
if int64(opsLenGot) != expArgLen {
|
|
return errIncorrectArgPacking
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p Preparator) validateExpiration(fbTX *transaction.Transaction) error {
|
|
if len(fbTX.Attributes) != 3 {
|
|
return errIncorrectFBAttributesAmount
|
|
}
|
|
|
|
nvbAttrs := fbTX.GetAttributes(transaction.NotValidBeforeT)
|
|
if len(nvbAttrs) != 1 {
|
|
return errIncorrectFBAttributes
|
|
}
|
|
|
|
nvb, ok := nvbAttrs[0].Value.(*transaction.NotValidBefore)
|
|
if !ok {
|
|
return errIncorrectFBAttributes
|
|
}
|
|
|
|
currBlock, err := p.blockCounter.BlockCount()
|
|
if err != nil {
|
|
return fmt.Errorf("fetch current chain height: %w", err)
|
|
}
|
|
|
|
if currBlock >= nvb.Height {
|
|
return &ExpiredTXError{
|
|
CurrentBlockHeight: currBlock,
|
|
FallbackTXNotValidBeforeHeight: nvb.Height,
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p Preparator) validateCosigners(expected int, s []transaction.Signer, alphaKeys keys.PublicKeys) error {
|
|
if len(s) != expected {
|
|
return errUnexpectedCosignersAmount
|
|
}
|
|
|
|
alphaVerificationScript, err := smartcontract.CreateMultiSigRedeemScript(len(alphaKeys)*2/3+1, alphaKeys)
|
|
if err != nil {
|
|
return fmt.Errorf("get Alphabet verification script: %w", err)
|
|
}
|
|
|
|
if !s[1].Account.Equals(hash.Hash160(alphaVerificationScript)) {
|
|
return errIncorrectAlphabetSigner
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p Preparator) validateWitnesses(w []transaction.Witness, alphaKeys keys.PublicKeys, invokerWitness bool) error {
|
|
// the first one(proxy contract) must have empty
|
|
// witnesses
|
|
if len(w[0].VerificationScript)+len(w[0].InvocationScript) != 0 {
|
|
return errIncorrectProxyWitnesses
|
|
}
|
|
|
|
alphaVerificationScript, err := smartcontract.CreateMultiSigRedeemScript(len(alphaKeys)*2/3+1, alphaKeys)
|
|
if err != nil {
|
|
return fmt.Errorf("get Alphabet verification script: %w", err)
|
|
}
|
|
|
|
// the second one must be witness of the current
|
|
// alphabet multiaccount
|
|
if !bytes.Equal(w[1].VerificationScript, alphaVerificationScript) {
|
|
return errIncorrectAlphabet
|
|
}
|
|
|
|
if invokerWitness {
|
|
// the optional third one must be an invoker witness
|
|
if len(w[2].VerificationScript)+len(w[2].InvocationScript) == 0 {
|
|
return errIncorrectInvokerWitnesses
|
|
}
|
|
}
|
|
|
|
// the last one must be a placeholder for notary contract witness
|
|
last := len(w) - 1
|
|
if !(len(w[last].InvocationScript) == 0 || // https://github.com/nspcc-dev/neo-go/pull/2981
|
|
bytes.Equal(w[last].InvocationScript, p.dummyInvocationScript)) || // compatibility with old version
|
|
len(w[last].VerificationScript) != 0 {
|
|
return errIncorrectNotaryPlaceholder
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p Preparator) validateAttributes(aa []transaction.Attribute, alphaKeys keys.PublicKeys, invokerWitness bool) error {
|
|
// main tx must have exactly one attribute
|
|
if len(aa) != 1 {
|
|
return errIncorrectAttributesAmount
|
|
}
|
|
|
|
expectedN := uint8(len(alphaKeys))
|
|
if invokerWitness {
|
|
expectedN++
|
|
}
|
|
|
|
val, ok := aa[0].Value.(*transaction.NotaryAssisted)
|
|
if !ok || val.NKeys != expectedN {
|
|
return errIncorrectAttribute
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type parsedNotaryEvent struct {
|
|
hash util.Uint160
|
|
notaryType NotaryType
|
|
params []Op
|
|
raw *payload.P2PNotaryRequest
|
|
}
|
|
|
|
func (p parsedNotaryEvent) ScriptHash() util.Uint160 {
|
|
return p.hash
|
|
}
|
|
|
|
func (p parsedNotaryEvent) Type() NotaryType {
|
|
return p.notaryType
|
|
}
|
|
|
|
func (p parsedNotaryEvent) Params() []Op {
|
|
return p.params
|
|
}
|
|
|
|
func (p parsedNotaryEvent) Raw() *payload.P2PNotaryRequest {
|
|
return p.raw
|
|
}
|