forked from TrueCloudLab/frostfs-node
[#932] adm: Refactor command morph
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
This commit is contained in:
parent
0bd030507e
commit
cda3a3d834
26 changed files with 324 additions and 290 deletions
15
cmd/frostfs-adm/internal/modules/morph/util/const.go
Normal file
15
cmd/frostfs-adm/internal/modules/morph/util/const.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package util
|
||||
|
||||
const (
|
||||
ConsensusAccountName = "consensus"
|
||||
ProtoConfigPath = "protocol"
|
||||
|
||||
// MaxAlphabetNodes is the maximum number of candidates allowed, which is currently limited by the size
|
||||
// of the invocation script.
|
||||
// See: https://github.com/nspcc-dev/neo-go/blob/740488f7f35e367eaa99a71c0a609c315fe2b0fc/pkg/core/transaction/witness.go#L10
|
||||
MaxAlphabetNodes = 22
|
||||
|
||||
EndpointFlag = "rpc-endpoint"
|
||||
EndpointFlagDesc = "N3 RPC node endpoint"
|
||||
EndpointFlagShort = "r"
|
||||
)
|
90
cmd/frostfs-adm/internal/modules/morph/util/initialize.go
Normal file
90
cmd/frostfs-adm/internal/modules/morph/util/initialize.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var ErrTooManyAlphabetNodes = fmt.Errorf("too many alphabet nodes (maximum allowed is %d)", MaxAlphabetNodes)
|
||||
|
||||
func AwaitTx(cmd *cobra.Command, c Client, txs []HashVUBPair) error {
|
||||
cmd.Println("Waiting for transactions to persist...")
|
||||
|
||||
at := trigger.Application
|
||||
|
||||
var retErr error
|
||||
|
||||
loop:
|
||||
for i := range txs {
|
||||
var it int
|
||||
var pollInterval time.Duration
|
||||
var pollIntervalChanged bool
|
||||
for {
|
||||
// We must fetch current height before application log, to avoid race condition.
|
||||
currBlock, err := c.GetBlockCount()
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't fetch current block height: %w", err)
|
||||
}
|
||||
res, err := c.GetApplicationLog(txs[i].Hash, &at)
|
||||
if err == nil {
|
||||
if retErr == nil && len(res.Executions) > 0 && res.Executions[0].VMState != vmstate.Halt {
|
||||
retErr = fmt.Errorf("tx %d persisted in %s state: %s",
|
||||
i, res.Executions[0].VMState, res.Executions[0].FaultException)
|
||||
}
|
||||
continue loop
|
||||
}
|
||||
if txs[i].Vub < currBlock {
|
||||
return fmt.Errorf("tx was not persisted: Vub=%d, height=%d", txs[i].Vub, currBlock)
|
||||
}
|
||||
|
||||
pollInterval, pollIntervalChanged = NextPollInterval(it, pollInterval)
|
||||
if pollIntervalChanged && viper.GetBool(commonflags.Verbose) {
|
||||
cmd.Printf("Pool interval to check transaction persistence changed: %s\n", pollInterval.String())
|
||||
}
|
||||
|
||||
timer := time.NewTimer(pollInterval)
|
||||
select {
|
||||
case <-cmd.Context().Done():
|
||||
return cmd.Context().Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
|
||||
it++
|
||||
}
|
||||
}
|
||||
|
||||
return retErr
|
||||
}
|
||||
|
||||
func NextPollInterval(it int, previous time.Duration) (time.Duration, bool) {
|
||||
const minPollInterval = 1 * time.Second
|
||||
const maxPollInterval = 16 * time.Second
|
||||
const changeAfter = 5
|
||||
if it == 0 {
|
||||
return minPollInterval, true
|
||||
}
|
||||
if it%changeAfter != 0 {
|
||||
return previous, false
|
||||
}
|
||||
nextInterval := previous * 2
|
||||
if nextInterval > maxPollInterval {
|
||||
return maxPollInterval, previous != maxPollInterval
|
||||
}
|
||||
return nextInterval, true
|
||||
}
|
||||
|
||||
func GetWalletAccount(w *wallet.Wallet, typ string) (*wallet.Account, error) {
|
||||
for i := range w.Accounts {
|
||||
if w.Accounts[i].Label == typ {
|
||||
return w.Accounts[i], nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("account for '%s' not found", typ)
|
||||
}
|
403
cmd/frostfs-adm/internal/modules/morph/util/local_client.go
Normal file
403
cmd/frostfs-adm/internal/modules/morph/util/local_client.go
Normal file
|
@ -0,0 +1,403 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"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
|
||||
maxGasInvoke int64
|
||||
}
|
||||
|
||||
func NewLocalClient(cmd *cobra.Command, v *viper.Viper, wallets []*wallet.Wallet, dumpPath string) (*LocalClient, error) {
|
||||
cfg, err := config.LoadFile(v.GetString(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], 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],
|
||||
maxGasInvoke: 15_0000_0000,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *LocalClient) GetBlockCount() (uint32, error) {
|
||||
return l.bc.BlockHeight(), nil
|
||||
}
|
||||
|
||||
func (l *LocalClient) GetNativeContracts() ([]state.NativeContract, 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 := 0; j < nSigs; j++ {
|
||||
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
|
||||
}
|
121
cmd/frostfs-adm/internal/modules/morph/util/n3client.go
Normal file
121
cmd/frostfs-adm/internal/modules/morph/util/n3client.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||
"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/neorpc/result"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Client represents N3 client interface capable of test-invoking scripts
|
||||
// and sending signed transactions to chain.
|
||||
type Client interface {
|
||||
invoker.RPCInvoke
|
||||
|
||||
GetBlockCount() (uint32, error)
|
||||
GetNativeContracts() ([]state.NativeContract, error)
|
||||
GetApplicationLog(util.Uint256, *trigger.Type) (*result.ApplicationLog, error)
|
||||
GetVersion() (*result.Version, error)
|
||||
SendRawTransaction(*transaction.Transaction) (util.Uint256, error)
|
||||
GetCommittee() (keys.PublicKeys, error)
|
||||
CalculateNetworkFee(tx *transaction.Transaction) (int64, error)
|
||||
}
|
||||
|
||||
type HashVUBPair struct {
|
||||
Hash util.Uint256
|
||||
Vub uint32
|
||||
}
|
||||
|
||||
type ClientContext struct {
|
||||
Client Client // a raw neo-go client OR a local chain implementation
|
||||
CommitteeAct *actor.Actor // committee actor with the Global witness scope
|
||||
ReadOnlyInvoker *invoker.Invoker // R/O contract invoker, does not contain any signer
|
||||
SentTxs []HashVUBPair
|
||||
}
|
||||
|
||||
func GetN3Client(v *viper.Viper) (Client, error) {
|
||||
// number of opened connections
|
||||
// by neo-go client per one host
|
||||
const (
|
||||
maxConnsPerHost = 10
|
||||
requestTimeout = time.Second * 10
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
endpoint := v.GetString(EndpointFlag)
|
||||
if endpoint == "" {
|
||||
return nil, errors.New("missing endpoint")
|
||||
}
|
||||
c, err := rpcclient.New(ctx, endpoint, rpcclient.Options{
|
||||
MaxConnsPerHost: maxConnsPerHost,
|
||||
RequestTimeout: requestTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.Init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func DefaultClientContext(c Client, committeeAcc *wallet.Account) (*ClientContext, error) {
|
||||
commAct, err := NewActor(c, committeeAcc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ClientContext{
|
||||
Client: c,
|
||||
CommitteeAct: commAct,
|
||||
ReadOnlyInvoker: invoker.New(c, nil),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *ClientContext) SendTx(tx *transaction.Transaction, cmd *cobra.Command, await bool) error {
|
||||
h, err := c.Client.SendRawTransaction(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if h != tx.Hash() {
|
||||
return fmt.Errorf("sent and actual tx hashes mismatch:\n\tsent: %v\n\tactual: %v", tx.Hash().StringLE(), h.StringLE())
|
||||
}
|
||||
|
||||
c.SentTxs = append(c.SentTxs, HashVUBPair{Hash: h, Vub: tx.ValidUntilBlock})
|
||||
|
||||
if await {
|
||||
return c.AwaitTx(cmd)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClientContext) AwaitTx(cmd *cobra.Command) error {
|
||||
if len(c.SentTxs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if local, ok := c.Client.(*LocalClient); ok {
|
||||
if err := local.putTransactions(); err != nil {
|
||||
return fmt.Errorf("can't persist transactions: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err := AwaitTx(cmd, c.Client, c.SentTxs)
|
||||
c.SentTxs = c.SentTxs[:0]
|
||||
|
||||
return err
|
||||
}
|
86
cmd/frostfs-adm/internal/modules/morph/util/util.go
Normal file
86
cmd/frostfs-adm/internal/modules/morph/util/util.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring"
|
||||
"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/rpcclient/actor"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func GetAlphabetWallets(v *viper.Viper, walletDir string) ([]*wallet.Wallet, error) {
|
||||
wallets, err := OpenAlphabetWallets(v, walletDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(wallets) > MaxAlphabetNodes {
|
||||
return nil, ErrTooManyAlphabetNodes
|
||||
}
|
||||
return wallets, nil
|
||||
}
|
||||
|
||||
func OpenAlphabetWallets(v *viper.Viper, walletDir string) ([]*wallet.Wallet, error) {
|
||||
walletFiles, err := os.ReadDir(walletDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't read alphabet wallets dir: %w", err)
|
||||
}
|
||||
|
||||
var size int
|
||||
loop:
|
||||
for i := 0; i < len(walletFiles); i++ {
|
||||
name := innerring.GlagoliticLetter(i).String() + ".json"
|
||||
for j := range walletFiles {
|
||||
if walletFiles[j].Name() == name {
|
||||
size++
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
if size == 0 {
|
||||
return nil, errors.New("alphabet wallets dir is empty (run `generate-alphabet` command first)")
|
||||
}
|
||||
|
||||
wallets := make([]*wallet.Wallet, size)
|
||||
for i := 0; i < size; i++ {
|
||||
letter := innerring.GlagoliticLetter(i).String()
|
||||
p := filepath.Join(walletDir, letter+".json")
|
||||
w, err := wallet.NewWalletFromFile(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't open wallet: %w", err)
|
||||
}
|
||||
|
||||
password, err := config.GetPassword(v, letter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't fetch password: %w", err)
|
||||
}
|
||||
|
||||
for i := range w.Accounts {
|
||||
if err := w.Accounts[i].Decrypt(password, keys.NEP2ScryptParams()); err != nil {
|
||||
return nil, fmt.Errorf("can't unlock wallet: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
wallets[i] = w
|
||||
}
|
||||
|
||||
return wallets, nil
|
||||
}
|
||||
|
||||
func NewActor(c actor.RPCActor, committeeAcc *wallet.Account) (*actor.Actor, error) {
|
||||
return actor.New(c, []actor.SignerAccount{{
|
||||
Signer: transaction.Signer{
|
||||
Account: committeeAcc.Contract.ScriptHash(),
|
||||
Scopes: transaction.Global,
|
||||
},
|
||||
Account: committeeAcc,
|
||||
}})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue