package helper import ( "crypto/elliptic" "errors" "fmt" "os" "sort" "time" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants" "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/chaindump" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" ) type LocalClient struct { bc *core.Blockchain transactions []*transaction.Transaction dumpPath string accounts []*wallet.Account } func NewLocalClient(cmd *cobra.Command, v *viper.Viper, wallets []*wallet.Wallet, dumpPath string) (*LocalClient, error) { cfg, err := config.LoadFile(v.GetString(commonflags.ProtoConfigPath)) if err != nil { return nil, err } bc, err := core.NewBlockchain(storage.NewMemoryStore(), cfg.Blockchain(), zap.NewNop()) if err != nil { return nil, err } m := smartcontract.GetDefaultHonestNodeCount(int(cfg.ProtocolConfiguration.ValidatorsCount)) accounts := make([]*wallet.Account, len(wallets)) for i := range accounts { accounts[i], err = GetWalletAccount(wallets[i], constants.ConsensusAccountName) if err != nil { return nil, err } } indexMap := make(map[string]int) for i, pub := range cfg.ProtocolConfiguration.StandbyCommittee { indexMap[pub] = i } sort.Slice(accounts, func(i, j int) bool { pi := accounts[i].PrivateKey().PublicKey().Bytes() pj := accounts[j].PrivateKey().PublicKey().Bytes() return indexMap[string(pi)] < indexMap[string(pj)] }) sort.Slice(accounts[:cfg.ProtocolConfiguration.ValidatorsCount], func(i, j int) bool { return accounts[i].PublicKey().Cmp(accounts[j].PublicKey()) == -1 }) go bc.Run() if cmd.Name() != "init" { f, err := os.OpenFile(dumpPath, os.O_RDONLY, 0o600) if err != nil { return nil, fmt.Errorf("can't open local dump: %w", err) } defer f.Close() r := io.NewBinReaderFromIO(f) var skip uint32 if bc.BlockHeight() != 0 { skip = bc.BlockHeight() + 1 } count := r.ReadU32LE() - skip if err := chaindump.Restore(bc, r, skip, count, nil); err != nil { return nil, fmt.Errorf("can't restore local dump: %w", err) } } return &LocalClient{ bc: bc, dumpPath: dumpPath, accounts: accounts[:m], }, nil } func (l *LocalClient) GetBlockCount() (uint32, error) { return l.bc.BlockHeight(), nil } func (l *LocalClient) GetNativeContracts() ([]state.Contract, error) { return l.bc.GetNatives(), nil } func (l *LocalClient) GetApplicationLog(h util.Uint256, t *trigger.Type) (*result.ApplicationLog, error) { aer, err := l.bc.GetAppExecResults(h, *t) if err != nil { return nil, err } a := result.NewApplicationLog(h, aer, *t) return &a, nil } func (l *LocalClient) GetCommittee() (keys.PublicKeys, error) { // not used by `morph init` command panic("unexpected call") } // InvokeFunction is implemented via `InvokeScript`. func (l *LocalClient) InvokeFunction(h util.Uint160, method string, sPrm []smartcontract.Parameter, ss []transaction.Signer) (*result.Invoke, error) { var err error pp := make([]any, len(sPrm)) for i, p := range sPrm { pp[i], err = smartcontract.ExpandParameterToEmitable(p) if err != nil { return nil, fmt.Errorf("incorrect parameter type %s: %w", p.Type, err) } } return InvokeFunction(l, h, method, pp, ss) } func (l *LocalClient) TerminateSession(_ uuid.UUID) (bool, error) { // not used by `morph init` command panic("unexpected call") } func (l *LocalClient) TraverseIterator(_, _ uuid.UUID, _ int) ([]stackitem.Item, error) { // not used by `morph init` command panic("unexpected call") } // GetVersion return default version. func (l *LocalClient) GetVersion() (*result.Version, error) { c := l.bc.GetConfig() return &result.Version{ Protocol: result.Protocol{ AddressVersion: address.NEO3Prefix, Network: c.Magic, MillisecondsPerBlock: int(c.TimePerBlock / time.Millisecond), MaxTraceableBlocks: c.MaxTraceableBlocks, MaxValidUntilBlockIncrement: c.MaxValidUntilBlockIncrement, MaxTransactionsPerBlock: c.MaxTransactionsPerBlock, MemoryPoolMaxTransactions: c.MemPoolSize, ValidatorsCount: byte(c.ValidatorsCount), InitialGasDistribution: c.InitialGASSupply, CommitteeHistory: c.CommitteeHistory, P2PSigExtensions: c.P2PSigExtensions, StateRootInHeader: c.StateRootInHeader, ValidatorsHistory: c.ValidatorsHistory, }, }, nil } func (l *LocalClient) InvokeContractVerify(util.Uint160, []smartcontract.Parameter, []transaction.Signer, ...transaction.Witness) (*result.Invoke, error) { // not used by `morph init` command panic("unexpected call") } // CalculateNetworkFee calculates network fee for the given transaction. // Copied from neo-go with minor corrections (no need to support non-notary mode): // https://github.com/nspcc-dev/neo-go/blob/v0.103.0/pkg/services/rpcsrv/server.go#L911 func (l *LocalClient) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) { // Avoid setting hash for this tx: server code doesn't touch client transaction. data := tx.Bytes() tx, err := transaction.NewTransactionFromBytes(data) if err != nil { return 0, err } hashablePart, err := tx.EncodeHashableFields() if err != nil { return 0, err } size := len(hashablePart) + io.GetVarSize(len(tx.Signers)) var ( netFee int64 // Verification GAS cost can't exceed this policy. gasLimit = l.bc.GetMaxVerificationGAS() ) for i, signer := range tx.Signers { w := tx.Scripts[i] if len(w.InvocationScript) == 0 { // No invocation provided, try to infer one. var paramz []manifest.Parameter if len(w.VerificationScript) == 0 { // Contract-based verification cs := l.bc.GetContractState(signer.Account) if cs == nil { return 0, fmt.Errorf("signer %d has no verification script and no deployed contract", i) } md := cs.Manifest.ABI.GetMethod(manifest.MethodVerify, -1) if md == nil || md.ReturnType != smartcontract.BoolType { return 0, fmt.Errorf("signer %d has no verify method in deployed contract", i) } paramz = md.Parameters // Might as well have none params and it's OK. } else { // Regular signature verification. if vm.IsSignatureContract(w.VerificationScript) { paramz = []manifest.Parameter{{Type: smartcontract.SignatureType}} } else if nSigs, _, ok := vm.ParseMultiSigContract(w.VerificationScript); ok { paramz = make([]manifest.Parameter, nSigs) for j := range nSigs { paramz[j] = manifest.Parameter{Type: smartcontract.SignatureType} } } } inv := io.NewBufBinWriter() for _, p := range paramz { p.Type.EncodeDefaultValue(inv.BinWriter) } if inv.Err != nil { return 0, fmt.Errorf("failed to create dummy invocation script (signer %d): %s", i, inv.Err.Error()) } w.InvocationScript = inv.Bytes() } gasConsumed, err := l.bc.VerifyWitness(signer.Account, tx, &w, gasLimit) if err != nil && !errors.Is(err, core.ErrInvalidSignature) { return 0, err } gasLimit -= gasConsumed netFee += gasConsumed size += io.GetVarSize(w.VerificationScript) + io.GetVarSize(w.InvocationScript) } if l.bc.P2PSigExtensionsEnabled() { attrs := tx.GetAttributes(transaction.NotaryAssistedT) if len(attrs) != 0 { na := attrs[0].Value.(*transaction.NotaryAssisted) netFee += (int64(na.NKeys) + 1) * l.bc.GetNotaryServiceFeePerKey() } } fee := l.bc.FeePerByte() netFee += int64(size) * fee return netFee, nil } func (l *LocalClient) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) { lastBlock, err := l.bc.GetBlock(l.bc.CurrentBlockHash()) if err != nil { return nil, err } tx := transaction.New(script, 0) tx.Signers = signers tx.ValidUntilBlock = l.bc.BlockHeight() + 2 ic, err := l.bc.GetTestVM(trigger.Application, tx, &block.Block{ Header: block.Header{ Index: lastBlock.Index + 1, Timestamp: lastBlock.Timestamp + 1, }, }) if err != nil { return nil, fmt.Errorf("get test VM: %w", err) } ic.VM.GasLimit = 100_0000_0000 ic.VM.LoadScriptWithFlags(script, callflag.All) var errStr string if err := ic.VM.Run(); err != nil { errStr = err.Error() } return &result.Invoke{ State: ic.VM.State().String(), GasConsumed: ic.VM.GasConsumed(), Script: script, Stack: ic.VM.Estack().ToArray(), FaultException: errStr, }, nil } func (l *LocalClient) SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) { // We need to test that transaction was formed correctly to catch as many errors as we can. bs := tx.Bytes() _, err := transaction.NewTransactionFromBytes(bs) if err != nil { return tx.Hash(), fmt.Errorf("invalid transaction: %w", err) } l.transactions = append(l.transactions, tx) return tx.Hash(), nil } func (l *LocalClient) putTransactions() error { // 1. Prepare new block. lastBlock, err := l.bc.GetBlock(l.bc.CurrentBlockHash()) if err != nil { panic(err) } defer func() { l.transactions = l.transactions[:0] }() b := &block.Block{ Header: block.Header{ NextConsensus: l.accounts[0].Contract.ScriptHash(), Script: transaction.Witness{ VerificationScript: l.accounts[0].Contract.Script, }, Timestamp: lastBlock.Timestamp + 1, }, Transactions: l.transactions, } if l.bc.GetConfig().StateRootInHeader { b.StateRootEnabled = true b.PrevStateRoot = l.bc.GetStateModule().CurrentLocalStateRoot() } b.PrevHash = lastBlock.Hash() b.Index = lastBlock.Index + 1 b.RebuildMerkleRoot() // 2. Sign prepared block. var invocationScript []byte magic := l.bc.GetConfig().Magic for _, acc := range l.accounts { sign := acc.PrivateKey().SignHashable(uint32(magic), b) invocationScript = append(invocationScript, byte(opcode.PUSHDATA1), 64) invocationScript = append(invocationScript, sign...) } b.Script.InvocationScript = invocationScript // 3. Persist block. return l.bc.AddBlock(b) } func InvokeFunction(c Client, h util.Uint160, method string, parameters []any, signers []transaction.Signer) (*result.Invoke, error) { w := io.NewBufBinWriter() emit.Array(w.BinWriter, parameters...) emit.AppCallNoArgs(w.BinWriter, h, method, callflag.All) if w.Err != nil { panic(fmt.Sprintf("BUG: invalid parameters for '%s': %v", method, w.Err)) } return c.InvokeScript(w.Bytes(), signers) } var errGetDesignatedByRoleResponse = errors.New("`getDesignatedByRole`: invalid response") func GetDesignatedByRole(inv *invoker.Invoker, h util.Uint160, role noderoles.Role, u uint32) (keys.PublicKeys, error) { arr, err := unwrap.Array(inv.Call(h, "getDesignatedByRole", int64(role), int64(u))) if err != nil { return nil, errGetDesignatedByRoleResponse } pubs := make(keys.PublicKeys, len(arr)) for i := range arr { bs, err := arr[i].TryBytes() if err != nil { return nil, errGetDesignatedByRoleResponse } pubs[i], err = keys.NewPublicKeyFromBytes(bs, elliptic.P256()) if err != nil { return nil, errGetDesignatedByRoleResponse } } return pubs, nil } func (l *LocalClient) Dump() (err error) { defer l.bc.Close() f, err := os.Create(l.dumpPath) if err != nil { return err } defer func() { closeErr := f.Close() if err == nil && closeErr != nil { err = closeErr } }() w := io.NewBinWriterFromIO(f) w.WriteU32LE(l.bc.BlockHeight() + 1) err = chaindump.Dump(l.bc, w, 0, l.bc.BlockHeight()+1) return }