From fbe347be4ce4e6df6dfb7367b74054160fe61052 Mon Sep 17 00:00:00 2001 From: Yilun Date: Fri, 18 Sep 2020 03:22:04 -0700 Subject: [PATCH] Reorganize tx validator to reduce go sdk memory usage on mobile Signed-off-by: Yilun --- .../{blockValidator.go => blockvalidator.go} | 212 +++++++- chain/{txValidator.go => bvs.go} | 466 +----------------- chain/mining.go | 5 +- chain/pool/txpool.go | 5 +- chain/txvalidator/txvalidator.go | 269 ++++++++++ node/relay.go | 5 +- node/transaction.go | 5 +- 7 files changed, 491 insertions(+), 476 deletions(-) rename chain/{blockValidator.go => blockvalidator.go} (69%) rename chain/{txValidator.go => bvs.go} (50%) create mode 100644 chain/txvalidator/txvalidator.go diff --git a/chain/blockValidator.go b/chain/blockvalidator.go similarity index 69% rename from chain/blockValidator.go rename to chain/blockvalidator.go index cf8d73039..0192d9633 100644 --- a/chain/blockValidator.go +++ b/chain/blockvalidator.go @@ -3,6 +3,7 @@ package chain import ( "bytes" "context" + "encoding/hex" "errors" "fmt" "hash/fnv" @@ -10,14 +11,16 @@ import ( "github.com/gogo/protobuf/proto" "github.com/nknorg/nkn/v2/block" + "github.com/nknorg/nkn/v2/chain/txvalidator" "github.com/nknorg/nkn/v2/common" + "github.com/nknorg/nkn/v2/config" "github.com/nknorg/nkn/v2/crypto" "github.com/nknorg/nkn/v2/pb" "github.com/nknorg/nkn/v2/por" + "github.com/nknorg/nkn/v2/program" "github.com/nknorg/nkn/v2/signature" "github.com/nknorg/nkn/v2/transaction" "github.com/nknorg/nkn/v2/util" - "github.com/nknorg/nkn/v2/config" "github.com/nknorg/nkn/v2/util/log" ) @@ -29,7 +32,12 @@ const ( NumGenesisBlocks = 4 ) -var timestampToleranceSalt []byte = util.RandomBytes(32) +var ( + ErrIDRegistered = errors.New("ID has be registered") + ErrDuplicateGenerateIDTxn = errors.New("[VerifyTransactionWithBlock], duplicate GenerateID txns") + ErrDuplicateIssueAssetTxn = errors.New("[VerifyTransactionWithBlock], duplicate IssueAsset txns") + timestampToleranceSalt = util.RandomBytes(32) +) type VBlock struct { Block *block.Block @@ -123,7 +131,7 @@ func TransactionCheck(ctx context.Context, block *block.Block) error { if i != 0 && txn.UnsignedTx.Payload.Type == pb.COINBASE_TYPE { return errors.New("Coinbase transaction order is incorrect") } - if err := VerifyTransaction(txn, block.Header.UnsignedHeader.Height); err != nil { + if err := txvalidator.VerifyTransaction(txn, block.Header.UnsignedHeader.Height); err != nil { return fmt.Errorf("transaction sanity check failed: %v", err) } if err := bvs.VerifyTransactionWithBlock(txn, block.Header.UnsignedHeader.Height); err != nil { @@ -496,3 +504,201 @@ func VerifyHeader(header *block.Header) bool { return true } + +// VerifyTransactionWithLedger verifys a transaction with history transaction in ledger +func VerifyTransactionWithLedger(txn *transaction.Transaction, height uint32) error { + if DefaultLedger.Store.IsDoubleSpend(txn) { + return errors.New("[VerifyTransactionWithLedger] IsDoubleSpend check faild") + } + + if DefaultLedger.Store.IsTxHashDuplicate(txn.Hash()) { + return errors.New("[VerifyTransactionWithLedger] duplicate transaction check faild") + } + + payload, err := transaction.Unpack(txn.UnsignedTx.Payload) + if err != nil { + return errors.New("unpack transactiion's payload error") + } + + pg, err := txn.GetProgramHashes() + if err != nil { + return err + } + + switch txn.UnsignedTx.Payload.Type { + case pb.NANO_PAY_TYPE: + case pb.SIG_CHAIN_TXN_TYPE: + default: + if txn.UnsignedTx.Nonce < DefaultLedger.Store.GetNonce(pg[0]) { + return errors.New("nonce is too low") + } + } + + var amount int64 + + switch txn.UnsignedTx.Payload.Type { + case pb.COINBASE_TYPE: + donationAmount, err := DefaultLedger.Store.GetDonation() + if err != nil { + return err + } + + donationProgramhash, _ := common.ToScriptHash(config.DonationAddress) + amount := DefaultLedger.Store.GetBalance(donationProgramhash) + if amount < donationAmount { + return errors.New("not sufficient funds in doation account") + } + case pb.TRANSFER_ASSET_TYPE: + pld := payload.(*pb.TransferAsset) + amount += pld.Amount + case pb.SIG_CHAIN_TXN_TYPE: + case pb.REGISTER_NAME_TYPE: + pld := payload.(*pb.RegisterName) + if config.LegacyNameService.GetValueAtHeight(height) { + name, err := DefaultLedger.Store.GetName_legacy(pld.Registrant) + if name != "" { + return fmt.Errorf("pubKey %s already has registered name %s", hex.EncodeToString(pld.Registrant), name) + } + if err != nil { + return err + } + + registrant, err := DefaultLedger.Store.GetRegistrant_legacy(pld.Name) + if err != nil { + return err + } + if registrant != nil { + return fmt.Errorf("name %s is already registered for pubKey %+v", pld.Name, registrant) + } + } else { + registrant, _, err := DefaultLedger.Store.GetRegistrant(pld.Name) + if err != nil { + return err + } + if len(registrant) > 0 && !bytes.Equal(registrant, pld.Registrant) { + return fmt.Errorf("name %s is already registered for pubKey %+v", pld.Name, registrant) + } + amount += pld.RegistrationFee + } + case pb.TRANSFER_NAME_TYPE: + pld := payload.(*pb.TransferName) + + registrant, _, err := DefaultLedger.Store.GetRegistrant(pld.Name) + if err != nil { + return err + } + if len(registrant) == 0 { + return fmt.Errorf("can not transfer unregistered name") + } + if bytes.Equal(registrant, pld.Recipient) { + return fmt.Errorf("can not transfer names to its owner") + } + if !bytes.Equal(registrant, pld.Registrant) { + return fmt.Errorf("registrant incorrect") + } + senderPubkey, err := program.GetPublicKeyFromCode(txn.Programs[0].Code) + if err != nil { + return err + } + if !bytes.Equal(registrant, senderPubkey) { + return fmt.Errorf("can not transfer names which did not belongs to you") + } + + case pb.DELETE_NAME_TYPE: + pld := payload.(*pb.DeleteName) + if config.LegacyNameService.GetValueAtHeight(height) { + name, err := DefaultLedger.Store.GetName_legacy(pld.Registrant) + if err != nil { + return err + } + if name == "" { + return fmt.Errorf("no name registered for pubKey %+v", pld.Registrant) + } else if name != pld.Name { + return fmt.Errorf("no name %s registered for pubKey %+v", pld.Name, pld.Registrant) + } + } else { + registrant, _, err := DefaultLedger.Store.GetRegistrant(pld.Name) + if err != nil { + return err + } + if len(registrant) == 0 { + return fmt.Errorf("name doesn't exist") + } + if !bytes.Equal(registrant, pld.Registrant) { + return fmt.Errorf("can not delete name which did not belongs to you") + } + } + + case pb.SUBSCRIBE_TYPE: + pld := payload.(*pb.Subscribe) + subscribed, err := DefaultLedger.Store.IsSubscribed(pld.Topic, pld.Bucket, pld.Subscriber, pld.Identifier) + if err != nil { + return err + } + if !subscribed { + subscriptionCount := DefaultLedger.Store.GetSubscribersCount(pld.Topic, pld.Bucket) + maxSubscriptionCount := config.MaxSubscriptionsCount + if subscriptionCount >= maxSubscriptionCount { + return fmt.Errorf("subscription count to %s can't be more than %d", pld.Topic, maxSubscriptionCount) + } + } + case pb.UNSUBSCRIBE_TYPE: + pld := payload.(*pb.Unsubscribe) + subscribed, err := DefaultLedger.Store.IsSubscribed(pld.Topic, 0, pld.Subscriber, pld.Identifier) + if err != nil { + return err + } + if !subscribed { + return fmt.Errorf("subscription to %s doesn't exist", pld.Topic) + } + case pb.GENERATE_ID_TYPE: + pld := payload.(*pb.GenerateID) + id, err := DefaultLedger.Store.GetID(pld.PublicKey) + if err != nil { + return err + } + if len(id) != 0 { + return ErrIDRegistered + } + amount += pld.RegistrationFee + case pb.NANO_PAY_TYPE: + pld := payload.(*pb.NanoPay) + + channelBalance, _, err := DefaultLedger.Store.GetNanoPay( + common.BytesToUint160(pld.Sender), + common.BytesToUint160(pld.Recipient), + pld.Id, + ) + if err != nil { + return err + } + + if height > pld.TxnExpiration { + return errors.New("nano pay txn has expired") + } + if height > pld.NanoPayExpiration { + return errors.New("nano pay has expired") + } + + balanceToClaim := pld.Amount - int64(channelBalance) + if balanceToClaim <= 0 { + return errors.New("invalid amount") + } + amount += balanceToClaim + case pb.ISSUE_ASSET_TYPE: + assetID := txn.Hash() + _, _, _, _, err := DefaultLedger.Store.GetAsset(assetID) + if err == nil { + return ErrDuplicateIssueAssetTxn + } + default: + return fmt.Errorf("invalid transaction payload type %v", txn.UnsignedTx.Payload.Type) + } + + balance := DefaultLedger.Store.GetBalance(pg[0]) + if int64(balance) < amount+txn.UnsignedTx.Fee { + return errors.New("not sufficient funds") + } + + return nil +} diff --git a/chain/txValidator.go b/chain/bvs.go similarity index 50% rename from chain/txValidator.go rename to chain/bvs.go index 1d4ee5f88..7f0bb490d 100644 --- a/chain/txValidator.go +++ b/chain/bvs.go @@ -1,482 +1,18 @@ package chain import ( - "bytes" "encoding/hex" "errors" - "fmt" - "regexp" "sync" "github.com/nknorg/nkn/v2/common" - "github.com/nknorg/nkn/v2/crypto/ed25519" - "github.com/nknorg/nkn/v2/program" - - "github.com/nknorg/nkn/v2/crypto" + "github.com/nknorg/nkn/v2/config" "github.com/nknorg/nkn/v2/pb" "github.com/nknorg/nkn/v2/transaction" "github.com/nknorg/nkn/v2/util/address" - "github.com/nknorg/nkn/v2/config" -) - -var ( - ErrIDRegistered = errors.New("ID has be registered") - ErrDuplicateGenerateIDTxn = errors.New("[VerifyTransactionWithBlock], duplicate GenerateID txns") - ErrDuplicateIssueAssetTxn = errors.New("[VerifyTransactionWithBlock], duplicate IssueAsset txns") ) -// VerifyTransaction verifys received single transaction -func VerifyTransaction(txn *transaction.Transaction, height uint32) error { - if err := CheckTransactionSize(txn); err != nil { - return fmt.Errorf("[VerifyTransaction] %v", err) - } - - if err := CheckAmount(txn.UnsignedTx.Fee); err != nil { - return fmt.Errorf("[VerifyTransaction] fee %v", err) - } - - if err := CheckTransactionNonce(txn); err != nil { - return fmt.Errorf("[VerifyTransaction] %v", err) - } - - if err := CheckTransactionAttribute(txn); err != nil { - return fmt.Errorf("[VerifyTransaction] %v", err) - } - - if err := txn.VerifySignature(); err != nil { - return fmt.Errorf("[VerifyTransaction] %v", err) - } - - if err := CheckTransactionPayload(txn, height); err != nil { - return fmt.Errorf("[VerifyTransaction] %v", err) - } - - return nil -} - -func CheckTransactionSize(txn *transaction.Transaction) error { - size := txn.GetSize() - if size <= 0 || size > config.MaxBlockSize { - return fmt.Errorf("invalid transaction size: %d bytes", size) - } - - return nil -} - -func CheckAmount(amount int64) error { - if amount < 0 { - return fmt.Errorf("amount %d is less than 0", amount) - } - - if amount > config.InitialIssueAmount+config.TotalMiningRewards { - return fmt.Errorf("amount %d is greater than max supply", amount) - } - - return nil -} - -func CheckTransactionNonce(txn *transaction.Transaction) error { - return nil -} - -func CheckTransactionAttribute(txn *transaction.Transaction) error { - maxAttrsLen := config.MaxTxnAttributesLen - if len(txn.UnsignedTx.Attributes) > maxAttrsLen { - return fmt.Errorf("attributes len %d is greater than %d", len(txn.UnsignedTx.Attributes), maxAttrsLen) - } - return nil -} - -func verifyPubSubTopic(topic string, height uint32) error { - regexPattern := config.AllowSubscribeTopicRegex.GetValueAtHeight(height) - match, err := regexp.MatchString(regexPattern, topic) - if err != nil { - return err - } - if !match { - return fmt.Errorf("topic %s should match %s", topic, regexPattern) - } - return nil - -} - -func CheckTransactionPayload(txn *transaction.Transaction, height uint32) error { - payload, err := transaction.Unpack(txn.UnsignedTx.Payload) - if err != nil { - return err - } - - switch txn.UnsignedTx.Payload.Type { - case pb.COINBASE_TYPE: - pld := payload.(*pb.Coinbase) - if len(pld.Sender) != common.UINT160SIZE && len(pld.Recipient) != common.UINT160SIZE { - return errors.New("length of programhash error") - } - - donationProgramhash, _ := common.ToScriptHash(config.DonationAddress) - if common.BytesToUint160(pld.Sender) != donationProgramhash { - return errors.New("invalid sender") - } - - if err = CheckAmount(pld.Amount); err != nil { - return err - } - case pb.TRANSFER_ASSET_TYPE: - pld := payload.(*pb.TransferAsset) - if len(pld.Sender) != common.UINT160SIZE && len(pld.Recipient) != common.UINT160SIZE { - return errors.New("length of programhash error") - } - - donationProgramhash, _ := common.ToScriptHash(config.DonationAddress) - if bytes.Equal(pld.Sender, donationProgramhash[:]) { - return errors.New("illegal transaction sender") - } - - if err = CheckAmount(pld.Amount); err != nil { - return err - } - case pb.SIG_CHAIN_TXN_TYPE: - case pb.REGISTER_NAME_TYPE: - if ok := config.AllowTxnRegisterName.GetValueAtHeight(height); !ok { - return errors.New("Register name transaction is not supported yet") - } - - pld := payload.(*pb.RegisterName) - if !config.LegacyNameService.GetValueAtHeight(height) { - if err = CheckAmount(pld.RegistrationFee); err != nil { - return err - } - if common.Fixed64(pld.RegistrationFee) < common.Fixed64(config.MinNameRegistrationFee) { - return fmt.Errorf("registration fee %s is lower than MinNameRegistrationFee %d", string(pld.Registrant), config.MinNameRegistrationFee) - } - } - regexPattern := config.AllowNameRegex.GetValueAtHeight(height) - match, err := regexp.MatchString(regexPattern, pld.Name) - if err != nil { - return err - } - if !match { - return fmt.Errorf("name %s should match regex %s", pld.Name, regexPattern) - } - case pb.TRANSFER_NAME_TYPE: - pld := payload.(*pb.TransferName) - if len(pld.Registrant) != ed25519.PublicKeySize { - return fmt.Errorf("registrant invalid") - } - case pb.DELETE_NAME_TYPE: - pld := payload.(*pb.DeleteName) - if len(pld.Registrant) != ed25519.PublicKeySize { - return fmt.Errorf("registrant invalid") - } - case pb.SUBSCRIBE_TYPE: - pld := payload.(*pb.Subscribe) - - if pld.Duration == 0 { - return fmt.Errorf("subscribe duration should be greater than 0") - } - - maxSubscribeBucket := config.MaxSubscribeBucket.GetValueAtHeight(height) - if pld.Bucket > uint32(maxSubscribeBucket) { - return fmt.Errorf("subscribe bucket %d is greater than %d", pld.Bucket, maxSubscribeBucket) - } - - maxDuration := config.MaxSubscribeDuration.GetValueAtHeight(height) - if pld.Duration > uint32(maxDuration) { - return fmt.Errorf("subscribe duration %d is greater than %d", pld.Duration, maxDuration) - } - - if err = verifyPubSubTopic(pld.Topic, height); err != nil { - return err - } - - maxIdentifierLen := config.MaxSubscribeIdentifierLen.GetValueAtHeight(height) - if len(pld.Identifier) > int(maxIdentifierLen) { - return fmt.Errorf("subscribe identifier len %d is greater than %d", len(pld.Identifier), maxIdentifierLen) - } - - maxMetaLen := config.MaxSubscribeMetaLen.GetValueAtHeight(height) - if len(pld.Meta) > int(maxMetaLen) { - return fmt.Errorf("subscribe meta len %d is greater than %d", len(pld.Meta), maxMetaLen) - } - case pb.UNSUBSCRIBE_TYPE: - pld := payload.(*pb.Unsubscribe) - - if err := verifyPubSubTopic(pld.Topic, height); err != nil { - return err - } - case pb.GENERATE_ID_TYPE: - pld := payload.(*pb.GenerateID) - err := crypto.CheckPublicKey(pld.PublicKey) - if err != nil { - return fmt.Errorf("decode pubkey error: %v", err) - } - - if err = CheckAmount(pld.RegistrationFee); err != nil { - return err - } - - if common.Fixed64(pld.RegistrationFee) < common.Fixed64(config.MinGenIDRegistrationFee) { - return errors.New("registration fee is lower than MinGenIDRegistrationFee") - } - - txnHash := txn.Hash() - if txnHash.CompareTo(config.MaxGenerateIDTxnHash.GetValueAtHeight(height)) > 0 { - return errors.New("txn hash is greater than MaxGenerateIDTxnHash") - } - case pb.NANO_PAY_TYPE: - pld := payload.(*pb.NanoPay) - - if len(pld.Sender) != common.UINT160SIZE && len(pld.Recipient) != common.UINT160SIZE { - return errors.New("length of programhash error") - } - - donationProgramhash, _ := common.ToScriptHash(config.DonationAddress) - if bytes.Equal(pld.Sender, donationProgramhash[:]) { - return errors.New("illegal transaction sender") - } - - if err = CheckAmount(pld.Amount); err != nil { - return err - } - - if pld.TxnExpiration > pld.NanoPayExpiration { - return errors.New("txn expiration should be no later than nano pay expiration") - } - - case pb.ISSUE_ASSET_TYPE: - pld := payload.(*pb.IssueAsset) - if len(pld.Sender) != common.UINT160SIZE { - return errors.New("length of programhash error") - } - - match, err := regexp.MatchString("(^[A-Za-z][A-Za-z0-9 ]{2,11}$)", pld.Name) - if err != nil { - return err - } - if !match { - return fmt.Errorf("name %s should start with a letter, contain A-Za-z0-9 and have length 3-12", pld.Name) - } - - match, err = regexp.MatchString("(^[a-z][a-z0-9]{2,8}$)", pld.Symbol) - if err != nil { - return err - } - if !match { - return fmt.Errorf("name %s should start with a letter, contain a-z0-9 and have length 3-9", pld.Name) - } - - if pld.TotalSupply < 0 { - return fmt.Errorf("TotalSupply %v should be a positive number", pld.TotalSupply) - } - - if pld.Precision > config.MaxAssetPrecision { - return fmt.Errorf("Precision %v should less than %v", pld.Precision, config.MaxAssetPrecision) - } - default: - return fmt.Errorf("invalid transaction payload type %v", txn.UnsignedTx.Payload.Type) - } - return nil -} - -// VerifyTransactionWithLedger verifys a transaction with history transaction in ledger -func VerifyTransactionWithLedger(txn *transaction.Transaction, height uint32) error { - if DefaultLedger.Store.IsDoubleSpend(txn) { - return errors.New("[VerifyTransactionWithLedger] IsDoubleSpend check faild") - } - - if DefaultLedger.Store.IsTxHashDuplicate(txn.Hash()) { - return errors.New("[VerifyTransactionWithLedger] duplicate transaction check faild") - } - - payload, err := transaction.Unpack(txn.UnsignedTx.Payload) - if err != nil { - return errors.New("unpack transactiion's payload error") - } - - pg, err := txn.GetProgramHashes() - if err != nil { - return err - } - - switch txn.UnsignedTx.Payload.Type { - case pb.NANO_PAY_TYPE: - case pb.SIG_CHAIN_TXN_TYPE: - default: - if txn.UnsignedTx.Nonce < DefaultLedger.Store.GetNonce(pg[0]) { - return errors.New("nonce is too low") - } - } - - var amount int64 - - switch txn.UnsignedTx.Payload.Type { - case pb.COINBASE_TYPE: - donationAmount, err := DefaultLedger.Store.GetDonation() - if err != nil { - return err - } - - donationProgramhash, _ := common.ToScriptHash(config.DonationAddress) - amount := DefaultLedger.Store.GetBalance(donationProgramhash) - if amount < donationAmount { - return errors.New("not sufficient funds in doation account") - } - case pb.TRANSFER_ASSET_TYPE: - pld := payload.(*pb.TransferAsset) - amount += pld.Amount - case pb.SIG_CHAIN_TXN_TYPE: - case pb.REGISTER_NAME_TYPE: - pld := payload.(*pb.RegisterName) - if config.LegacyNameService.GetValueAtHeight(height) { - name, err := DefaultLedger.Store.GetName_legacy(pld.Registrant) - if name != "" { - return fmt.Errorf("pubKey %s already has registered name %s", hex.EncodeToString(pld.Registrant), name) - } - if err != nil { - return err - } - - registrant, err := DefaultLedger.Store.GetRegistrant_legacy(pld.Name) - if err != nil { - return err - } - if registrant != nil { - return fmt.Errorf("name %s is already registered for pubKey %+v", pld.Name, registrant) - } - } else { - registrant, _, err := DefaultLedger.Store.GetRegistrant(pld.Name) - if err != nil { - return err - } - if len(registrant) > 0 && !bytes.Equal(registrant, pld.Registrant) { - return fmt.Errorf("name %s is already registered for pubKey %+v", pld.Name, registrant) - } - amount += pld.RegistrationFee - } - case pb.TRANSFER_NAME_TYPE: - pld := payload.(*pb.TransferName) - - registrant, _, err := DefaultLedger.Store.GetRegistrant(pld.Name) - if err != nil { - return err - } - if len(registrant) == 0 { - return fmt.Errorf("can not transfer unregistered name") - } - if bytes.Equal(registrant, pld.Recipient) { - return fmt.Errorf("can not transfer names to its owner") - } - if !bytes.Equal(registrant, pld.Registrant) { - return fmt.Errorf("registrant incorrect") - } - senderPubkey, err := program.GetPublicKeyFromCode(txn.Programs[0].Code) - if err != nil { - return err - } - if !bytes.Equal(registrant, senderPubkey) { - return fmt.Errorf("can not transfer names which did not belongs to you") - } - - case pb.DELETE_NAME_TYPE: - pld := payload.(*pb.DeleteName) - if config.LegacyNameService.GetValueAtHeight(height) { - name, err := DefaultLedger.Store.GetName_legacy(pld.Registrant) - if err != nil { - return err - } - if name == "" { - return fmt.Errorf("no name registered for pubKey %+v", pld.Registrant) - } else if name != pld.Name { - return fmt.Errorf("no name %s registered for pubKey %+v", pld.Name, pld.Registrant) - } - } else { - registrant, _, err := DefaultLedger.Store.GetRegistrant(pld.Name) - if err != nil { - return err - } - if len(registrant) == 0 { - return fmt.Errorf("name doesn't exist") - } - if !bytes.Equal(registrant, pld.Registrant) { - return fmt.Errorf("can not delete name which did not belongs to you") - } - } - - case pb.SUBSCRIBE_TYPE: - pld := payload.(*pb.Subscribe) - subscribed, err := DefaultLedger.Store.IsSubscribed(pld.Topic, pld.Bucket, pld.Subscriber, pld.Identifier) - if err != nil { - return err - } - if !subscribed { - subscriptionCount := DefaultLedger.Store.GetSubscribersCount(pld.Topic, pld.Bucket) - maxSubscriptionCount := config.MaxSubscriptionsCount - if subscriptionCount >= maxSubscriptionCount { - return fmt.Errorf("subscription count to %s can't be more than %d", pld.Topic, maxSubscriptionCount) - } - } - case pb.UNSUBSCRIBE_TYPE: - pld := payload.(*pb.Unsubscribe) - subscribed, err := DefaultLedger.Store.IsSubscribed(pld.Topic, 0, pld.Subscriber, pld.Identifier) - if err != nil { - return err - } - if !subscribed { - return fmt.Errorf("subscription to %s doesn't exist", pld.Topic) - } - case pb.GENERATE_ID_TYPE: - pld := payload.(*pb.GenerateID) - id, err := DefaultLedger.Store.GetID(pld.PublicKey) - if err != nil { - return err - } - if len(id) != 0 { - return ErrIDRegistered - } - amount += pld.RegistrationFee - case pb.NANO_PAY_TYPE: - pld := payload.(*pb.NanoPay) - - channelBalance, _, err := DefaultLedger.Store.GetNanoPay( - common.BytesToUint160(pld.Sender), - common.BytesToUint160(pld.Recipient), - pld.Id, - ) - if err != nil { - return err - } - - if height > pld.TxnExpiration { - return errors.New("nano pay txn has expired") - } - if height > pld.NanoPayExpiration { - return errors.New("nano pay has expired") - } - - balanceToClaim := pld.Amount - int64(channelBalance) - if balanceToClaim <= 0 { - return errors.New("invalid amount") - } - amount += balanceToClaim - case pb.ISSUE_ASSET_TYPE: - assetID := txn.Hash() - _, _, _, _, err := DefaultLedger.Store.GetAsset(assetID) - if err == nil { - return ErrDuplicateIssueAssetTxn - } - default: - return fmt.Errorf("invalid transaction payload type %v", txn.UnsignedTx.Payload.Type) - } - - balance := DefaultLedger.Store.GetBalance(pg[0]) - if int64(balance) < amount+txn.UnsignedTx.Fee { - return errors.New("not sufficient funds") - } - - return nil -} - type subscription struct { topic string bucket uint32 diff --git a/chain/mining.go b/chain/mining.go index 2470dbd97..36081a8cd 100644 --- a/chain/mining.go +++ b/chain/mining.go @@ -4,14 +4,15 @@ import ( "context" "github.com/nknorg/nkn/v2/block" + "github.com/nknorg/nkn/v2/chain/txvalidator" "github.com/nknorg/nkn/v2/common" + "github.com/nknorg/nkn/v2/config" "github.com/nknorg/nkn/v2/crypto" "github.com/nknorg/nkn/v2/pb" "github.com/nknorg/nkn/v2/por" "github.com/nknorg/nkn/v2/signature" "github.com/nknorg/nkn/v2/transaction" "github.com/nknorg/nkn/v2/util" - "github.com/nknorg/nkn/v2/config" "github.com/nknorg/nkn/v2/util/log" "github.com/nknorg/nkn/v2/vault" ) @@ -110,7 +111,7 @@ func (bm *BuiltinMining) BuildBlock(ctx context.Context, height uint32, chordID break } - if err := VerifyTransaction(txn, height); err != nil { + if err := txvalidator.VerifyTransaction(txn, height); err != nil { log.Warningf("invalid transaction: %v", err) txnCollection.Pop() continue diff --git a/chain/pool/txpool.go b/chain/pool/txpool.go index bd70c9f1d..c83fb4d93 100644 --- a/chain/pool/txpool.go +++ b/chain/pool/txpool.go @@ -10,10 +10,11 @@ import ( "time" "github.com/nknorg/nkn/v2/chain" + "github.com/nknorg/nkn/v2/chain/txvalidator" "github.com/nknorg/nkn/v2/common" + "github.com/nknorg/nkn/v2/config" "github.com/nknorg/nkn/v2/pb" "github.com/nknorg/nkn/v2/transaction" - "github.com/nknorg/nkn/v2/config" "github.com/nknorg/nkn/v2/util/log" ) @@ -241,7 +242,7 @@ func (tp *TxnPool) AppendTxnPool(txn *transaction.Transaction) error { } // 2. verify txn - if err := chain.VerifyTransaction(txn, chain.DefaultLedger.Store.GetHeight()+1); err != nil { + if err := txvalidator.VerifyTransaction(txn, chain.DefaultLedger.Store.GetHeight()+1); err != nil { return err } diff --git a/chain/txvalidator/txvalidator.go b/chain/txvalidator/txvalidator.go new file mode 100644 index 000000000..2ef0befa1 --- /dev/null +++ b/chain/txvalidator/txvalidator.go @@ -0,0 +1,269 @@ +package txvalidator + +import ( + "bytes" + "errors" + "fmt" + "regexp" + + "github.com/nknorg/nkn/v2/common" + "github.com/nknorg/nkn/v2/crypto/ed25519" + + "github.com/nknorg/nkn/v2/config" + "github.com/nknorg/nkn/v2/crypto" + "github.com/nknorg/nkn/v2/pb" + "github.com/nknorg/nkn/v2/transaction" +) + +// VerifyTransaction verifys received single transaction +func VerifyTransaction(txn *transaction.Transaction, height uint32) error { + if err := CheckTransactionSize(txn); err != nil { + return fmt.Errorf("[VerifyTransaction] %v", err) + } + + if err := CheckAmount(txn.UnsignedTx.Fee); err != nil { + return fmt.Errorf("[VerifyTransaction] fee %v", err) + } + + if err := CheckTransactionNonce(txn); err != nil { + return fmt.Errorf("[VerifyTransaction] %v", err) + } + + if err := CheckTransactionAttribute(txn); err != nil { + return fmt.Errorf("[VerifyTransaction] %v", err) + } + + if err := txn.VerifySignature(); err != nil { + return fmt.Errorf("[VerifyTransaction] %v", err) + } + + if err := CheckTransactionPayload(txn, height); err != nil { + return fmt.Errorf("[VerifyTransaction] %v", err) + } + + return nil +} + +func CheckTransactionSize(txn *transaction.Transaction) error { + size := txn.GetSize() + if size <= 0 || size > config.MaxBlockSize { + return fmt.Errorf("invalid transaction size: %d bytes", size) + } + + return nil +} + +func CheckAmount(amount int64) error { + if amount < 0 { + return fmt.Errorf("amount %d is less than 0", amount) + } + + if amount > config.InitialIssueAmount+config.TotalMiningRewards { + return fmt.Errorf("amount %d is greater than max supply", amount) + } + + return nil +} + +func CheckTransactionNonce(txn *transaction.Transaction) error { + return nil +} + +func CheckTransactionAttribute(txn *transaction.Transaction) error { + maxAttrsLen := config.MaxTxnAttributesLen + if len(txn.UnsignedTx.Attributes) > maxAttrsLen { + return fmt.Errorf("attributes len %d is greater than %d", len(txn.UnsignedTx.Attributes), maxAttrsLen) + } + return nil +} + +func verifyPubSubTopic(topic string, height uint32) error { + regexPattern := config.AllowSubscribeTopicRegex.GetValueAtHeight(height) + match, err := regexp.MatchString(regexPattern, topic) + if err != nil { + return err + } + if !match { + return fmt.Errorf("topic %s should match %s", topic, regexPattern) + } + return nil + +} + +func CheckTransactionPayload(txn *transaction.Transaction, height uint32) error { + payload, err := transaction.Unpack(txn.UnsignedTx.Payload) + if err != nil { + return err + } + + switch txn.UnsignedTx.Payload.Type { + case pb.COINBASE_TYPE: + pld := payload.(*pb.Coinbase) + if len(pld.Sender) != common.UINT160SIZE && len(pld.Recipient) != common.UINT160SIZE { + return errors.New("length of programhash error") + } + + donationProgramhash, _ := common.ToScriptHash(config.DonationAddress) + if common.BytesToUint160(pld.Sender) != donationProgramhash { + return errors.New("invalid sender") + } + + if err = CheckAmount(pld.Amount); err != nil { + return err + } + case pb.TRANSFER_ASSET_TYPE: + pld := payload.(*pb.TransferAsset) + if len(pld.Sender) != common.UINT160SIZE && len(pld.Recipient) != common.UINT160SIZE { + return errors.New("length of programhash error") + } + + donationProgramhash, _ := common.ToScriptHash(config.DonationAddress) + if bytes.Equal(pld.Sender, donationProgramhash[:]) { + return errors.New("illegal transaction sender") + } + + if err = CheckAmount(pld.Amount); err != nil { + return err + } + case pb.SIG_CHAIN_TXN_TYPE: + case pb.REGISTER_NAME_TYPE: + if ok := config.AllowTxnRegisterName.GetValueAtHeight(height); !ok { + return errors.New("Register name transaction is not supported yet") + } + + pld := payload.(*pb.RegisterName) + if !config.LegacyNameService.GetValueAtHeight(height) { + if err = CheckAmount(pld.RegistrationFee); err != nil { + return err + } + if common.Fixed64(pld.RegistrationFee) < common.Fixed64(config.MinNameRegistrationFee) { + return fmt.Errorf("registration fee %s is lower than MinNameRegistrationFee %d", string(pld.Registrant), config.MinNameRegistrationFee) + } + } + regexPattern := config.AllowNameRegex.GetValueAtHeight(height) + match, err := regexp.MatchString(regexPattern, pld.Name) + if err != nil { + return err + } + if !match { + return fmt.Errorf("name %s should match regex %s", pld.Name, regexPattern) + } + case pb.TRANSFER_NAME_TYPE: + pld := payload.(*pb.TransferName) + if len(pld.Registrant) != ed25519.PublicKeySize { + return fmt.Errorf("registrant invalid") + } + case pb.DELETE_NAME_TYPE: + pld := payload.(*pb.DeleteName) + if len(pld.Registrant) != ed25519.PublicKeySize { + return fmt.Errorf("registrant invalid") + } + case pb.SUBSCRIBE_TYPE: + pld := payload.(*pb.Subscribe) + + if pld.Duration == 0 { + return fmt.Errorf("subscribe duration should be greater than 0") + } + + maxSubscribeBucket := config.MaxSubscribeBucket.GetValueAtHeight(height) + if pld.Bucket > uint32(maxSubscribeBucket) { + return fmt.Errorf("subscribe bucket %d is greater than %d", pld.Bucket, maxSubscribeBucket) + } + + maxDuration := config.MaxSubscribeDuration.GetValueAtHeight(height) + if pld.Duration > uint32(maxDuration) { + return fmt.Errorf("subscribe duration %d is greater than %d", pld.Duration, maxDuration) + } + + if err = verifyPubSubTopic(pld.Topic, height); err != nil { + return err + } + + maxIdentifierLen := config.MaxSubscribeIdentifierLen.GetValueAtHeight(height) + if len(pld.Identifier) > int(maxIdentifierLen) { + return fmt.Errorf("subscribe identifier len %d is greater than %d", len(pld.Identifier), maxIdentifierLen) + } + + maxMetaLen := config.MaxSubscribeMetaLen.GetValueAtHeight(height) + if len(pld.Meta) > int(maxMetaLen) { + return fmt.Errorf("subscribe meta len %d is greater than %d", len(pld.Meta), maxMetaLen) + } + case pb.UNSUBSCRIBE_TYPE: + pld := payload.(*pb.Unsubscribe) + + if err := verifyPubSubTopic(pld.Topic, height); err != nil { + return err + } + case pb.GENERATE_ID_TYPE: + pld := payload.(*pb.GenerateID) + err := crypto.CheckPublicKey(pld.PublicKey) + if err != nil { + return fmt.Errorf("decode pubkey error: %v", err) + } + + if err = CheckAmount(pld.RegistrationFee); err != nil { + return err + } + + if common.Fixed64(pld.RegistrationFee) < common.Fixed64(config.MinGenIDRegistrationFee) { + return errors.New("registration fee is lower than MinGenIDRegistrationFee") + } + + txnHash := txn.Hash() + if txnHash.CompareTo(config.MaxGenerateIDTxnHash.GetValueAtHeight(height)) > 0 { + return errors.New("txn hash is greater than MaxGenerateIDTxnHash") + } + case pb.NANO_PAY_TYPE: + pld := payload.(*pb.NanoPay) + + if len(pld.Sender) != common.UINT160SIZE && len(pld.Recipient) != common.UINT160SIZE { + return errors.New("length of programhash error") + } + + donationProgramhash, _ := common.ToScriptHash(config.DonationAddress) + if bytes.Equal(pld.Sender, donationProgramhash[:]) { + return errors.New("illegal transaction sender") + } + + if err = CheckAmount(pld.Amount); err != nil { + return err + } + + if pld.TxnExpiration > pld.NanoPayExpiration { + return errors.New("txn expiration should be no later than nano pay expiration") + } + + case pb.ISSUE_ASSET_TYPE: + pld := payload.(*pb.IssueAsset) + if len(pld.Sender) != common.UINT160SIZE { + return errors.New("length of programhash error") + } + + match, err := regexp.MatchString("(^[A-Za-z][A-Za-z0-9 ]{2,11}$)", pld.Name) + if err != nil { + return err + } + if !match { + return fmt.Errorf("name %s should start with a letter, contain A-Za-z0-9 and have length 3-12", pld.Name) + } + + match, err = regexp.MatchString("(^[a-z][a-z0-9]{2,8}$)", pld.Symbol) + if err != nil { + return err + } + if !match { + return fmt.Errorf("name %s should start with a letter, contain a-z0-9 and have length 3-9", pld.Name) + } + + if pld.TotalSupply < 0 { + return fmt.Errorf("TotalSupply %v should be a positive number", pld.TotalSupply) + } + + if pld.Precision > config.MaxAssetPrecision { + return fmt.Errorf("Precision %v should less than %v", pld.Precision, config.MaxAssetPrecision) + } + default: + return fmt.Errorf("invalid transaction payload type %v", txn.UnsignedTx.Payload.Type) + } + return nil +} diff --git a/node/relay.go b/node/relay.go index e2f8807f7..35ea9e704 100644 --- a/node/relay.go +++ b/node/relay.go @@ -7,12 +7,13 @@ import ( "github.com/gogo/protobuf/proto" "github.com/nknorg/nkn/v2/block" "github.com/nknorg/nkn/v2/chain" + "github.com/nknorg/nkn/v2/chain/txvalidator" + "github.com/nknorg/nkn/v2/config" "github.com/nknorg/nkn/v2/event" "github.com/nknorg/nkn/v2/pb" "github.com/nknorg/nkn/v2/por" "github.com/nknorg/nkn/v2/transaction" "github.com/nknorg/nkn/v2/util/address" - "github.com/nknorg/nkn/v2/config" "github.com/nknorg/nkn/v2/util/log" "github.com/nknorg/nkn/v2/vault" ) @@ -250,7 +251,7 @@ func (rs *RelayService) broadcastSigChain(sigChain *pb.SigChain) error { currentHeight := chain.DefaultLedger.Store.GetHeight() - err = chain.VerifyTransaction(txn, currentHeight+1) + err = txvalidator.VerifyTransaction(txn, currentHeight+1) if err != nil { return err } diff --git a/node/transaction.go b/node/transaction.go index 4b9b4eb5d..39584a828 100644 --- a/node/transaction.go +++ b/node/transaction.go @@ -10,12 +10,13 @@ import ( "github.com/nknorg/nkn/v2/block" "github.com/nknorg/nkn/v2/chain" "github.com/nknorg/nkn/v2/chain/pool" + "github.com/nknorg/nkn/v2/chain/txvalidator" "github.com/nknorg/nkn/v2/common" + "github.com/nknorg/nkn/v2/config" "github.com/nknorg/nkn/v2/pb" "github.com/nknorg/nkn/v2/por" "github.com/nknorg/nkn/v2/transaction" "github.com/nknorg/nkn/v2/util" - "github.com/nknorg/nkn/v2/config" "github.com/nknorg/nkn/v2/util/log" nnetnode "github.com/nknorg/nnet/node" nnetpb "github.com/nknorg/nnet/protobuf" @@ -199,7 +200,7 @@ func (localNode *LocalNode) startRequestingSigChainTxn() { requestedHashCache.Set(info.hash, struct{}{}) - err = chain.VerifyTransaction(txn, currentHeight+1) + err = txvalidator.VerifyTransaction(txn, currentHeight+1) if err != nil { log.Warningf("Verify sigchain txn error: %v", err) continue