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 }