github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/services/stateroot/service_test.go (about)

     1  package stateroot_test
     2  
     3  import (
     4  	"crypto/elliptic"
     5  	"path/filepath"
     6  	"sort"
     7  	"sync/atomic"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/nspcc-dev/neo-go/internal/basicchain"
    12  	"github.com/nspcc-dev/neo-go/internal/testserdes"
    13  	"github.com/nspcc-dev/neo-go/pkg/config"
    14  	"github.com/nspcc-dev/neo-go/pkg/config/netmode"
    15  	"github.com/nspcc-dev/neo-go/pkg/core"
    16  	"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
    17  	"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
    18  	"github.com/nspcc-dev/neo-go/pkg/core/state"
    19  	corestate "github.com/nspcc-dev/neo-go/pkg/core/stateroot"
    20  	"github.com/nspcc-dev/neo-go/pkg/core/storage"
    21  	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
    22  	"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
    23  	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
    24  	"github.com/nspcc-dev/neo-go/pkg/interop/native/roles"
    25  	"github.com/nspcc-dev/neo-go/pkg/io"
    26  	"github.com/nspcc-dev/neo-go/pkg/neotest"
    27  	"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
    28  	"github.com/nspcc-dev/neo-go/pkg/network/payload"
    29  	"github.com/nspcc-dev/neo-go/pkg/services/stateroot"
    30  	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
    31  	"github.com/nspcc-dev/neo-go/pkg/util"
    32  	"github.com/nspcc-dev/neo-go/pkg/vm/emit"
    33  	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
    34  	"github.com/nspcc-dev/neo-go/pkg/wallet"
    35  	"github.com/stretchr/testify/require"
    36  	"go.uber.org/zap/zaptest"
    37  )
    38  
    39  func testSignStateRoot(t *testing.T, r *state.MPTRoot, pubs keys.PublicKeys, accs ...*wallet.Account) []byte {
    40  	n := smartcontract.GetMajorityHonestNodeCount(len(accs))
    41  	w := io.NewBufBinWriter()
    42  	for i := 0; i < n; i++ {
    43  		sig := accs[i].PrivateKey().SignHashable(uint32(netmode.UnitTestNet), r)
    44  		emit.Bytes(w.BinWriter, sig)
    45  	}
    46  	require.NoError(t, w.Err)
    47  
    48  	script, err := smartcontract.CreateMajorityMultiSigRedeemScript(pubs.Copy())
    49  	require.NoError(t, err)
    50  	r.Witness = []transaction.Witness{{
    51  		VerificationScript: script,
    52  		InvocationScript:   w.Bytes(),
    53  	}}
    54  	data, err := testserdes.EncodeBinary(stateroot.NewMessage(stateroot.RootT, r))
    55  	require.NoError(t, err)
    56  	return data
    57  }
    58  
    59  func newMajorityMultisigWithGAS(t *testing.T, n int) (util.Uint160, keys.PublicKeys, []*wallet.Account) {
    60  	accs := make([]*wallet.Account, n)
    61  	for i := range accs {
    62  		acc, err := wallet.NewAccount()
    63  		require.NoError(t, err)
    64  		accs[i] = acc
    65  	}
    66  	sort.Slice(accs, func(i, j int) bool {
    67  		pi := accs[i].PublicKey()
    68  		pj := accs[j].PublicKey()
    69  		return pi.Cmp(pj) == -1
    70  	})
    71  	pubs := make(keys.PublicKeys, n)
    72  	for i := range pubs {
    73  		pubs[i] = accs[i].PublicKey()
    74  	}
    75  	script, err := smartcontract.CreateMajorityMultiSigRedeemScript(pubs)
    76  	require.NoError(t, err)
    77  	return hash.Hash160(script), pubs, accs
    78  }
    79  
    80  func TestStateRoot(t *testing.T) {
    81  	bc, validator, committee := chain.NewMulti(t)
    82  	e := neotest.NewExecutor(t, bc, validator, committee)
    83  	designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee)
    84  	gasValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas))
    85  
    86  	h, pubs, accs := newMajorityMultisigWithGAS(t, 2)
    87  	validatorNodes := []any{pubs[0].Bytes(), pubs[1].Bytes()}
    88  	designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole",
    89  		int64(roles.StateValidator), validatorNodes)
    90  	updateIndex := bc.BlockHeight()
    91  
    92  	gasValidatorInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), h, 1_0000_0000, nil)
    93  
    94  	tmpDir := t.TempDir()
    95  	w := createAndWriteWallet(t, accs[0], filepath.Join(tmpDir, "w"), "pass")
    96  	cfg := createStateRootConfig(w.Path(), "pass")
    97  	srMod := bc.GetStateModule().(*corestate.Module) // Take full responsibility here.
    98  	srv, err := stateroot.New(cfg, srMod, zaptest.NewLogger(t), bc, nil)
    99  	require.NoError(t, err)
   100  	require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight())
   101  	r, err := bc.GetStateModule().GetStateRoot(bc.BlockHeight())
   102  	require.NoError(t, err)
   103  	require.Equal(t, r.Root, bc.GetStateModule().CurrentLocalStateRoot())
   104  
   105  	t.Run("invalid message", func(t *testing.T) {
   106  		require.Error(t, srv.OnPayload(&payload.Extensible{Data: []byte{42}}))
   107  		require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight())
   108  	})
   109  	t.Run("drop zero index", func(t *testing.T) {
   110  		r, err := bc.GetStateModule().GetStateRoot(0)
   111  		require.NoError(t, err)
   112  		data, err := testserdes.EncodeBinary(stateroot.NewMessage(stateroot.RootT, r))
   113  		require.NoError(t, err)
   114  		require.NoError(t, srv.OnPayload(&payload.Extensible{Data: data}))
   115  		require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight())
   116  	})
   117  	t.Run("invalid height", func(t *testing.T) {
   118  		r, err := bc.GetStateModule().GetStateRoot(1)
   119  		require.NoError(t, err)
   120  		r.Index = 10
   121  		data := testSignStateRoot(t, r, pubs, accs...)
   122  		require.Error(t, srv.OnPayload(&payload.Extensible{Data: data}))
   123  		require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight())
   124  	})
   125  	t.Run("invalid signer", func(t *testing.T) {
   126  		accInv, err := wallet.NewAccount()
   127  		require.NoError(t, err)
   128  		pubs := keys.PublicKeys{accInv.PublicKey()}
   129  		require.NoError(t, accInv.ConvertMultisig(1, pubs))
   130  		gasValidatorInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), accInv.Contract.ScriptHash(), 1_0000_0000, nil)
   131  		r, err := bc.GetStateModule().GetStateRoot(1)
   132  		require.NoError(t, err)
   133  		data := testSignStateRoot(t, r, pubs, accInv)
   134  		err = srv.OnPayload(&payload.Extensible{Data: data})
   135  		require.ErrorIs(t, err, core.ErrWitnessHashMismatch)
   136  		require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight())
   137  	})
   138  
   139  	r, err = bc.GetStateModule().GetStateRoot(updateIndex + 1)
   140  	require.NoError(t, err)
   141  	data := testSignStateRoot(t, r, pubs, accs...)
   142  	require.NoError(t, srv.OnPayload(&payload.Extensible{Data: data}))
   143  	require.EqualValues(t, 2, bc.GetStateModule().CurrentValidatedHeight())
   144  
   145  	r, err = bc.GetStateModule().GetStateRoot(updateIndex + 1)
   146  	require.NoError(t, err)
   147  	require.NotEqual(t, 0, len(r.Witness))
   148  	require.Equal(t, h, r.Witness[0].ScriptHash())
   149  }
   150  
   151  func TestStateRoot_GenesisRole(t *testing.T) {
   152  	_, _, accs := newMajorityMultisigWithGAS(t, 2)
   153  
   154  	bc, _, _ := chain.NewMultiWithCustomConfig(t, func(c *config.Blockchain) {
   155  		c.Genesis.Roles = map[noderoles.Role]keys.PublicKeys{
   156  			noderoles.StateValidator: {accs[0].PublicKey(), accs[1].PublicKey()},
   157  		}
   158  	})
   159  
   160  	tmpDir := t.TempDir()
   161  	w := createAndWriteWallet(t, accs[0], filepath.Join(tmpDir, "w"), "pass")
   162  	cfg := createStateRootConfig(w.Path(), "pass")
   163  	srMod := bc.GetStateModule().(*corestate.Module) // Take full responsibility here.
   164  	srv, err := stateroot.New(cfg, srMod, zaptest.NewLogger(t), bc, nil)
   165  	require.NoError(t, err)
   166  
   167  	require.True(t, srv.IsAuthorized())
   168  }
   169  
   170  type memoryStore struct {
   171  	*storage.MemoryStore
   172  }
   173  
   174  func (memoryStore) Close() error { return nil }
   175  
   176  func TestStateRootInitNonZeroHeight(t *testing.T) {
   177  	st := memoryStore{storage.NewMemoryStore()}
   178  	h, pubs, accs := newMajorityMultisigWithGAS(t, 2)
   179  
   180  	var root util.Uint256
   181  	t.Run("init", func(t *testing.T) { // this is in a separate test to do proper cleanup
   182  		bc, validator, committee := chain.NewMultiWithCustomConfigAndStore(t, nil, st, true)
   183  		e := neotest.NewExecutor(t, bc, validator, committee)
   184  		designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee)
   185  		gasValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas))
   186  
   187  		validatorNodes := []any{pubs[0].Bytes(), pubs[1].Bytes()}
   188  		designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole",
   189  			int64(roles.StateValidator), validatorNodes)
   190  		gasValidatorInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), h, 1_0000_0000, nil)
   191  
   192  		tmpDir := t.TempDir()
   193  		w := createAndWriteWallet(t, accs[0], filepath.Join(tmpDir, "w"), "pass")
   194  		cfg := createStateRootConfig(w.Path(), "pass")
   195  		srMod := bc.GetStateModule().(*corestate.Module) // Take full responsibility here.
   196  		srv, err := stateroot.New(cfg, srMod, zaptest.NewLogger(t), bc, nil)
   197  		require.NoError(t, err)
   198  		r, err := bc.GetStateModule().GetStateRoot(2)
   199  		require.NoError(t, err)
   200  		data := testSignStateRoot(t, r, pubs, accs...)
   201  		require.NoError(t, srv.OnPayload(&payload.Extensible{Data: data}))
   202  		require.EqualValues(t, 2, bc.GetStateModule().CurrentValidatedHeight())
   203  		root = bc.GetStateModule().CurrentLocalStateRoot()
   204  	})
   205  
   206  	bc2, _, _ := chain.NewMultiWithCustomConfigAndStore(t, nil, st, true)
   207  	srv := bc2.GetStateModule()
   208  	require.EqualValues(t, 2, srv.CurrentValidatedHeight())
   209  	require.Equal(t, root, srv.CurrentLocalStateRoot())
   210  }
   211  
   212  func createAndWriteWallet(t *testing.T, acc *wallet.Account, path, password string) *wallet.Wallet {
   213  	w, err := wallet.NewWallet(path)
   214  	require.NoError(t, err)
   215  	require.NoError(t, acc.Encrypt(password, w.Scrypt))
   216  	w.AddAccount(acc)
   217  	require.NoError(t, w.Save())
   218  	return w
   219  }
   220  
   221  func createStateRootConfig(walletPath, password string) config.StateRoot {
   222  	return config.StateRoot{
   223  		Enabled: true,
   224  		UnlockWallet: config.Wallet{
   225  			Path:     walletPath,
   226  			Password: password,
   227  		},
   228  	}
   229  }
   230  
   231  func TestStateRootFull(t *testing.T) {
   232  	tmpDir := t.TempDir()
   233  	bc, validator, committee := chain.NewMulti(t)
   234  	e := neotest.NewExecutor(t, bc, validator, committee)
   235  	designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee)
   236  	gasValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas))
   237  
   238  	getDesignatedByRole := func(t *testing.T, h uint32) keys.PublicKeys {
   239  		res, err := designationSuperInvoker.TestInvoke(t, "getDesignatedByRole", int64(noderoles.StateValidator), h)
   240  		require.NoError(t, err)
   241  		nodes := res.Pop().Value().([]stackitem.Item)
   242  		pubs := make(keys.PublicKeys, len(nodes))
   243  		for i, node := range nodes {
   244  			pubs[i], err = keys.NewPublicKeyFromBytes(node.Value().([]byte), elliptic.P256())
   245  			require.NoError(t, err)
   246  		}
   247  		return pubs
   248  	}
   249  
   250  	h, pubs, accs := newMajorityMultisigWithGAS(t, 2)
   251  	w := createAndWriteWallet(t, accs[1], filepath.Join(tmpDir, "wallet2"), "two")
   252  	cfg := createStateRootConfig(w.Path(), "two")
   253  
   254  	var lastValidated atomic.Value
   255  	var lastHeight atomic.Uint32
   256  	srMod := bc.GetStateModule().(*corestate.Module) // Take full responsibility here.
   257  	srv, err := stateroot.New(cfg, srMod, zaptest.NewLogger(t), bc, func(ep *payload.Extensible) {
   258  		lastHeight.Store(ep.ValidBlockStart)
   259  		lastValidated.Store(ep)
   260  	})
   261  	require.NoError(t, err)
   262  	srv.Start()
   263  	t.Cleanup(srv.Shutdown)
   264  
   265  	validatorNodes := []any{pubs[0].Bytes(), pubs[1].Bytes()}
   266  	designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole",
   267  		int64(roles.StateValidator), validatorNodes)
   268  	gasValidatorInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), h, 1_0000_0000, nil)
   269  	require.Eventually(t, func() bool { return lastHeight.Load() == 2 }, time.Second, time.Millisecond)
   270  	checkVoteBroadcasted(t, bc, lastValidated.Load().(*payload.Extensible), 2, 1, getDesignatedByRole)
   271  	e.AddNewBlock(t)
   272  	require.Eventually(t, func() bool { return lastHeight.Load() == 3 }, time.Second, time.Millisecond)
   273  	checkVoteBroadcasted(t, bc, lastValidated.Load().(*payload.Extensible), 3, 1, getDesignatedByRole)
   274  
   275  	r, err := bc.GetStateModule().GetStateRoot(2)
   276  	require.NoError(t, err)
   277  	require.NoError(t, srv.AddSignature(2, 0, accs[0].PrivateKey().SignHashable(uint32(netmode.UnitTestNet), r)))
   278  	require.NotNil(t, lastValidated.Load().(*payload.Extensible))
   279  
   280  	msg := new(stateroot.Message)
   281  	require.NoError(t, testserdes.DecodeBinary(lastValidated.Load().(*payload.Extensible).Data, msg))
   282  	require.NotEqual(t, stateroot.RootT, msg.Type) // not a sender for this root
   283  
   284  	r, err = bc.GetStateModule().GetStateRoot(3)
   285  	require.NoError(t, err)
   286  	require.Error(t, srv.AddSignature(2, 0, accs[0].PrivateKey().SignHashable(uint32(netmode.UnitTestNet), r)))
   287  	require.NoError(t, srv.AddSignature(3, 0, accs[0].PrivateKey().SignHashable(uint32(netmode.UnitTestNet), r)))
   288  	require.NotNil(t, lastValidated.Load().(*payload.Extensible))
   289  
   290  	require.NoError(t, testserdes.DecodeBinary(lastValidated.Load().(*payload.Extensible).Data, msg))
   291  	require.Equal(t, stateroot.RootT, msg.Type)
   292  
   293  	actual := msg.Payload.(*state.MPTRoot)
   294  	require.Equal(t, r.Index, actual.Index)
   295  	require.Equal(t, r.Version, actual.Version)
   296  	require.Equal(t, r.Root, actual.Root)
   297  }
   298  
   299  func checkVoteBroadcasted(t *testing.T, bc *core.Blockchain, p *payload.Extensible,
   300  	height uint32, valIndex byte, getDesignatedByRole func(t *testing.T, h uint32) keys.PublicKeys) {
   301  	require.NotNil(t, p)
   302  	m := new(stateroot.Message)
   303  	require.NoError(t, testserdes.DecodeBinary(p.Data, m))
   304  	require.Equal(t, stateroot.VoteT, m.Type)
   305  	vote := m.Payload.(*stateroot.Vote)
   306  
   307  	srv := bc.GetStateModule()
   308  	r, err := srv.GetStateRoot(bc.BlockHeight())
   309  	require.NoError(t, err)
   310  	require.Equal(t, height, vote.Height)
   311  	require.Equal(t, int32(valIndex), vote.ValidatorIndex)
   312  
   313  	pubs := getDesignatedByRole(t, bc.BlockHeight())
   314  	require.True(t, len(pubs) > int(valIndex))
   315  	require.True(t, pubs[valIndex].VerifyHashable(vote.Signature, uint32(netmode.UnitTestNet), r))
   316  }
   317  
   318  func TestStateroot_GetLatestStateHeight(t *testing.T) {
   319  	bc, validators, committee := chain.NewMultiWithCustomConfig(t, func(c *config.Blockchain) {
   320  		c.P2PSigExtensions = true
   321  	})
   322  	e := neotest.NewExecutor(t, bc, validators, committee)
   323  	basicchain.Init(t, "../../../", e)
   324  
   325  	m := bc.GetStateModule()
   326  	for i := uint32(0); i < bc.BlockHeight(); i++ {
   327  		r, err := m.GetStateRoot(i)
   328  		require.NoError(t, err)
   329  		h, err := bc.GetStateModule().GetLatestStateHeight(r.Root)
   330  		require.NoError(t, err, i)
   331  		require.Equal(t, i, h)
   332  	}
   333  }