github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/session/pingpong/consumer_balance_tracker_test.go (about) 1 /* 2 * Copyright (C) 2019 The "MysteriumNetwork/node" Authors. 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 package pingpong 19 20 import ( 21 "errors" 22 "fmt" 23 "math/big" 24 "sync" 25 "testing" 26 "time" 27 28 "github.com/ethereum/go-ethereum/common" 29 "github.com/mysteriumnetwork/node/eventbus" 30 "github.com/mysteriumnetwork/node/identity" 31 "github.com/mysteriumnetwork/node/identity/registry" 32 "github.com/mysteriumnetwork/node/session/pingpong/event" 33 "github.com/mysteriumnetwork/payments/client" 34 "github.com/stretchr/testify/assert" 35 ) 36 37 var mockMystSCaddress = common.HexToAddress("0x0") 38 39 var initialBalance = big.NewInt(100000000) 40 41 var defaultWaitTime = 3 * time.Second 42 var defaultWaitInterval = 20 * time.Millisecond 43 44 var defaultCfg = ConsumerBalanceTrackerConfig{ 45 FastSync: PollConfig{ 46 Interval: time.Second * 1, 47 Timeout: time.Second * 6, 48 }, 49 LongSync: PollConfig{ 50 Interval: time.Minute, 51 }, 52 } 53 54 func TestConsumerBalanceTracker_Fresh_Registration(t *testing.T) { 55 id1 := identity.FromAddress("0x000000001") 56 id2 := identity.FromAddress("0x000000002") 57 assert.NotEqual(t, id1.Address, id2.Address) 58 59 bus := eventbus.New() 60 mcts := mockConsumerTotalsStorage{ 61 bus: bus, 62 res: big.NewInt(0), 63 } 64 bc := mockConsumerBalanceChecker{ 65 channelToReturn: client.ConsumerChannel{ 66 Balance: initialBalance, 67 Settled: big.NewInt(0), 68 }, 69 mystBalanceToReturn: big.NewInt(0), 70 } 71 calc := mockAddressProvider{} 72 73 mockBlockchainProvider := mockBlockchainInfoProvider{} 74 75 cbt := NewConsumerBalanceTracker(bus, &bc, &mcts, &mockconsumerInfoGetter{}, &mockTransactor{}, &mockRegistrationStatusProvider{}, &calc, &mockBlockchainProvider, defaultCfg) 76 77 err := cbt.Subscribe(bus) 78 assert.NoError(t, err) 79 80 bus.Publish(registry.AppTopicIdentityRegistration, registry.AppEventIdentityRegistration{ 81 ID: id1, 82 Status: registry.Registered, 83 ChainID: 1, 84 }) 85 bus.Publish(registry.AppTopicIdentityRegistration, registry.AppEventIdentityRegistration{ 86 ID: id2, 87 Status: registry.RegistrationError, 88 ChainID: 1, 89 }) 90 91 assert.Eventually(t, func() bool { 92 return cbt.GetBalance(1, id1).Cmp(initialBalance) == 0 93 }, defaultWaitTime, defaultWaitInterval) 94 95 assert.Eventually(t, func() bool { 96 return cbt.GetBalance(1, id2).Uint64() == 0 97 }, defaultWaitTime, defaultWaitInterval) 98 99 bus.Publish(identity.AppTopicIdentityUnlock, identity.AppEventIdentityUnlock{ 100 ChainID: 1, 101 ID: id2, 102 }) 103 104 assert.Eventually(t, func() bool { 105 return cbt.GetBalance(1, id2).Cmp(initialBalance) == 0 106 }, defaultWaitTime, defaultWaitInterval) 107 108 var promised = big.NewInt(100) 109 bus.Publish(event.AppTopicGrandTotalChanged, event.AppEventGrandTotalChanged{ 110 ChainID: 1, 111 ConsumerID: id1, 112 Current: promised, 113 }) 114 115 assert.Eventually(t, func() bool { 116 return cbt.GetBalance(1, id1).Cmp(new(big.Int).Sub(initialBalance, promised)) == 0 117 }, defaultWaitTime, defaultWaitInterval) 118 } 119 120 func TestConsumerBalanceTracker_Fast_Registration(t *testing.T) { 121 id1 := identity.FromAddress("0x000000001") 122 t.Run("Takes balance from hermes response", func(t *testing.T) { 123 bus := eventbus.New() 124 mcts := mockConsumerTotalsStorage{ 125 bus: bus, 126 } 127 bc := mockConsumerBalanceChecker{ 128 channelToReturn: client.ConsumerChannel{ 129 Balance: initialBalance, 130 Settled: big.NewInt(0), 131 }, 132 } 133 calc := mockAddressProvider{} 134 mockBlockchainProvider := mockBlockchainInfoProvider{} 135 136 var ba = big.NewInt(10000000) 137 cbt := NewConsumerBalanceTracker(bus, &bc, &mcts, &mockconsumerInfoGetter{}, &mockTransactor{ 138 statusToReturn: registry.TransactorStatusResponse{ 139 Status: registry.TransactorRegistrationEntryStatusCreated, 140 BountyAmount: ba, 141 ChainID: 1, 142 }, 143 }, &mockRegistrationStatusProvider{}, &calc, &mockBlockchainProvider, defaultCfg) 144 145 err := cbt.Subscribe(bus) 146 assert.NoError(t, err) 147 148 bus.Publish(registry.AppTopicIdentityRegistration, registry.AppEventIdentityRegistration{ 149 ID: id1, 150 Status: registry.InProgress, 151 ChainID: 1, 152 }) 153 154 assert.Eventually(t, func() bool { 155 return cbt.GetBalance(1, id1).Cmp(ba) == 0 156 }, defaultWaitTime, defaultWaitInterval) 157 }) 158 t.Run("Falls back to blockchain balance if no bounty is specified on transactor", func(t *testing.T) { 159 t.Skip() 160 bus := eventbus.New() 161 mcts := mockConsumerTotalsStorage{ 162 res: big.NewInt(0), 163 bus: bus, 164 } 165 var ba = big.NewInt(10000000) 166 bc := mockConsumerBalanceChecker{ 167 channelToReturn: client.ConsumerChannel{ 168 Balance: initialBalance, 169 Settled: big.NewInt(0), 170 }, 171 mystBalanceToReturn: ba, 172 } 173 calc := mockAddressProvider{} 174 mockBlockchainProvider := mockBlockchainInfoProvider{} 175 176 cbt := NewConsumerBalanceTracker(bus, &bc, &mcts, &mockconsumerInfoGetter{}, &mockTransactor{ 177 statusToReturn: registry.TransactorStatusResponse{ 178 Status: registry.TransactorRegistrationEntryStatusCreated, 179 BountyAmount: big.NewInt(0), 180 ChainID: 1, 181 }, 182 }, &mockRegistrationStatusProvider{}, &calc, &mockBlockchainProvider, defaultCfg) 183 184 err := cbt.Subscribe(bus) 185 assert.NoError(t, err) 186 187 bus.Publish(registry.AppTopicIdentityRegistration, registry.AppEventIdentityRegistration{ 188 ID: id1, 189 Status: registry.InProgress, 190 ChainID: 1, 191 }) 192 193 assert.Eventually(t, func() bool { 194 return cbt.GetBalance(1, id1).Cmp(ba) == 0 195 }, defaultWaitTime, defaultWaitInterval) 196 }) 197 } 198 199 func TestConsumerBalanceTracker_Handles_GrandTotalChanges(t *testing.T) { 200 id1 := identity.FromAddress("0x000000001") 201 var grandTotalPromised = big.NewInt(100) 202 bus := eventbus.New() 203 204 mcts := mockConsumerTotalsStorage{ 205 res: grandTotalPromised, 206 } 207 bc := mockConsumerBalanceChecker{ 208 channelToReturn: client.ConsumerChannel{ 209 Balance: initialBalance, 210 Settled: big.NewInt(0), 211 }, 212 } 213 calc := mockAddressProvider{} 214 mockBlockchainProvider := mockBlockchainInfoProvider{} 215 216 cbt := NewConsumerBalanceTracker(bus, &bc, &mcts, &mockconsumerInfoGetter{grandTotalPromised, new(big.Int)}, &mockTransactor{}, &mockRegistrationStatusProvider{}, &calc, &mockBlockchainProvider, defaultCfg) 217 218 err := cbt.Subscribe(bus) 219 assert.NoError(t, err) 220 bus.Publish(identity.AppTopicIdentityUnlock, identity.AppEventIdentityUnlock{ 221 ChainID: 1, 222 ID: id1, 223 }) 224 assert.Eventually(t, func() bool { 225 return cbt.GetBalance(1, id1).Cmp(new(big.Int).Sub(initialBalance, grandTotalPromised)) == 0 226 }, defaultWaitTime, defaultWaitInterval) 227 228 var diff = big.NewInt(10) 229 bus.Publish(event.AppTopicGrandTotalChanged, event.AppEventGrandTotalChanged{ 230 ChainID: 1, 231 ConsumerID: id1, 232 Current: new(big.Int).Add(grandTotalPromised, diff), 233 }) 234 235 assert.Eventually(t, func() bool { 236 div := new(big.Int).Sub(initialBalance, grandTotalPromised) 237 currentBalance := new(big.Int).Sub(div, diff) 238 return cbt.GetBalance(1, id1).Cmp(currentBalance) == 0 239 }, defaultWaitTime, defaultWaitInterval) 240 241 var diff2 = big.NewInt(20) 242 bus.Publish(event.AppTopicGrandTotalChanged, event.AppEventGrandTotalChanged{ 243 ChainID: 1, 244 ConsumerID: id1, 245 Current: new(big.Int).Add(grandTotalPromised, diff2), 246 }) 247 248 assert.Eventually(t, func() bool { 249 div := new(big.Int).Sub(initialBalance, grandTotalPromised) 250 currentBalance := new(big.Int).Sub(div, diff2) 251 return cbt.GetBalance(1, id1).Cmp(currentBalance) == 0 252 }, defaultWaitTime, defaultWaitInterval) 253 } 254 255 func TestConsumerBalanceTracker_FallsBackToTransactorIfInProgress(t *testing.T) { 256 id1 := identity.FromAddress("0x000000001") 257 var grandTotalPromised = new(big.Int) 258 bus := eventbus.New() 259 mcts := mockConsumerTotalsStorage{ 260 res: grandTotalPromised, 261 bus: bus, 262 } 263 bc := mockConsumerBalanceChecker{ 264 errToReturn: errors.New("no contract deployed"), 265 mystBalanceToReturn: initialBalance, 266 } 267 cfg := defaultCfg 268 cfg.LongSync.Interval = time.Millisecond * 300 269 calc := mockAddressProvider{} 270 mockBlockchainProvider := mockBlockchainInfoProvider{} 271 272 cbt := NewConsumerBalanceTracker(bus, &bc, &mcts, &mockconsumerInfoGetter{grandTotalPromised, new(big.Int)}, &mockTransactor{ 273 statusToReturn: registry.TransactorStatusResponse{ 274 Status: registry.TransactorRegistrationEntryStatusCreated, 275 ChainID: 1, 276 BountyAmount: big.NewInt(100), 277 }, 278 }, &mockRegistrationStatusProvider{ 279 map[string]mockRegistrationStatus{ 280 fmt.Sprintf("%d%s", 1, id1.Address): { 281 status: registry.InProgress, 282 }, 283 }, 284 }, &calc, &mockBlockchainProvider, cfg) 285 286 err := cbt.Subscribe(bus) 287 assert.NoError(t, err) 288 bus.Publish(identity.AppTopicIdentityUnlock, identity.AppEventIdentityUnlock{ 289 ChainID: 1, 290 ID: id1, 291 }) 292 assert.Eventually(t, func() bool { 293 return cbt.GetBalance(1, id1).Uint64() == 100 294 }, defaultWaitTime, defaultWaitInterval) 295 } 296 297 func TestConsumerBalanceTracker_UnregisteredBalanceReturned(t *testing.T) { 298 id1 := identity.FromAddress("0x000000001") 299 var grandTotalPromised = new(big.Int) 300 bus := eventbus.New() 301 mcts := mockConsumerTotalsStorage{ 302 res: grandTotalPromised, 303 bus: bus, 304 } 305 bc := mockConsumerBalanceChecker{ 306 mystBalanceToReturn: initialBalance, 307 errToReturn: errors.New("boom"), 308 } 309 calc := mockAddressProvider{} 310 mockBlockchainProvider := mockBlockchainInfoProvider{} 311 312 cbt := NewConsumerBalanceTracker(bus, &bc, &mcts, &mockconsumerInfoGetter{grandTotalPromised, new(big.Int)}, &mockTransactor{}, &mockRegistrationStatusProvider{ 313 map[string]mockRegistrationStatus{ 314 fmt.Sprintf("%d%s", 1, id1.Address): { 315 status: registry.Unregistered, 316 }, 317 }, 318 }, &calc, &mockBlockchainProvider, defaultCfg) 319 320 b := cbt.ForceBalanceUpdate(1, id1) 321 assert.Equal(t, initialBalance, b) 322 } 323 324 func TestConsumerBalanceTracker_InprogressUnregisteredBalanceReturnedWhenNoBounty(t *testing.T) { 325 id1 := identity.FromAddress("0x000000001") 326 var grandTotalPromised = new(big.Int) 327 bus := eventbus.New() 328 mcts := mockConsumerTotalsStorage{ 329 res: grandTotalPromised, 330 bus: bus, 331 } 332 bc := mockConsumerBalanceChecker{ 333 errToReturn: errors.New("no contract deployed"), 334 mystBalanceToReturn: initialBalance, 335 } 336 cfg := defaultCfg 337 cfg.LongSync.Interval = time.Millisecond * 300 338 calc := mockAddressProvider{} 339 mockBlockchainProvider := mockBlockchainInfoProvider{} 340 341 cbt := NewConsumerBalanceTracker(bus, &bc, &mcts, &mockconsumerInfoGetter{grandTotalPromised, new(big.Int)}, &mockTransactor{ 342 statusToReturn: registry.TransactorStatusResponse{ 343 Status: registry.TransactorRegistrationEntryStatusCreated, 344 ChainID: 1, 345 BountyAmount: big.NewInt(0), 346 }, 347 }, &mockRegistrationStatusProvider{ 348 map[string]mockRegistrationStatus{ 349 fmt.Sprintf("%d%s", 1, id1.Address): { 350 status: registry.InProgress, 351 }, 352 }, 353 }, &calc, &mockBlockchainProvider, cfg) 354 355 err := cbt.Subscribe(bus) 356 assert.NoError(t, err) 357 bus.Publish(identity.AppTopicIdentityUnlock, identity.AppEventIdentityUnlock{ 358 ChainID: 1, 359 ID: id1, 360 }) 361 assert.Eventually(t, func() bool { 362 return cbt.GetBalance(1, id1).Cmp(initialBalance) == 0 363 }, defaultWaitTime, defaultWaitInterval) 364 } 365 366 func TestConsumerBalanceTracker_RecoverGrandTotalPromisedSettledIsBiggerThanPromissedNotOffChain(t *testing.T) { 367 // make data race more likely to happen 368 for i := 0; i < 10; i++ { 369 id1 := identity.FromAddress("0x000000001") 370 grandTotalPromised := big.NewInt(10) 371 settledAmount := big.NewInt(11) 372 bus := eventbus.New() 373 mcts := NewConsumerTotalsStorage(bus) 374 bc := mockConsumerBalanceChecker{} 375 cfg := defaultCfg 376 cfg.LongSync.Interval = time.Millisecond * 300 377 calc := newMockAddressProvider() 378 calc.addrToReturn = id1.ToCommonAddress() 379 mockBlockchainProvider := &mockBlockchainInfoProvider{} 380 381 mockBlockchainProvider.AddConsumerChannelsHermes(1, id1.ToCommonAddress(), client.ConsumersHermes{ 382 Settled: big.NewInt(6), 383 }) 384 385 cbt := NewConsumerBalanceTracker(bus, &bc, mcts, &mockconsumerInfoGetter{grandTotalPromised, settledAmount}, &mockTransactor{}, &mockRegistrationStatusProvider{}, calc, mockBlockchainProvider, defaultCfg) 386 387 err := cbt.Subscribe(bus) 388 assert.NoError(t, err) 389 bus.Publish(identity.AppTopicIdentityUnlock, identity.AppEventIdentityUnlock{ 390 ChainID: 1, 391 ID: id1, 392 }) 393 assert.Eventually(t, func() bool { 394 savedBalance, _ := mcts.Get(1, id1, common.BigToAddress(big.NewInt(0))) 395 return savedBalance.Cmp(big.NewInt(6)) == 0 396 }, defaultWaitTime, defaultWaitInterval) 397 } 398 } 399 400 type mockConsumerBalanceChecker struct { 401 channelToReturn client.ConsumerChannel 402 errToReturn error 403 errLock sync.Mutex 404 405 mystBalanceToReturn *big.Int 406 mystBalanceError error 407 } 408 409 func (mcbc *mockConsumerBalanceChecker) getError() error { 410 mcbc.errLock.Lock() 411 defer mcbc.errLock.Unlock() 412 return mcbc.errToReturn 413 } 414 415 func (mcbc *mockConsumerBalanceChecker) setError(err error) { 416 mcbc.errLock.Lock() 417 defer mcbc.errLock.Unlock() 418 mcbc.errToReturn = err 419 } 420 421 func (mcbc *mockConsumerBalanceChecker) GetConsumerChannel(chainID int64, addr common.Address, mystSCAddress common.Address) (client.ConsumerChannel, error) { 422 return mcbc.channelToReturn, mcbc.getError() 423 } 424 425 func (mcbc *mockConsumerBalanceChecker) GetMystBalance(chainID int64, mystAddress, identity common.Address) (*big.Int, error) { 426 return mcbc.mystBalanceToReturn, mcbc.mystBalanceError 427 } 428 429 type mockconsumerInfoGetter struct { 430 amount *big.Int 431 settled *big.Int 432 } 433 434 func (mcig *mockconsumerInfoGetter) GetConsumerData(_ int64, _ string, _ time.Duration) (HermesUserInfo, error) { 435 return HermesUserInfo{ 436 Settled: mcig.settled, 437 LatestPromise: LatestPromise{ 438 Amount: mcig.amount, 439 }, 440 }, nil 441 } 442 443 func TestConsumerBalanceTracker_DoesNotBlockedOnEmptyBalancesList(t *testing.T) { 444 bus := eventbus.New() 445 mcts := mockConsumerTotalsStorage{bus: bus, res: big.NewInt(0)} 446 bc := mockConsumerBalanceChecker{ 447 channelToReturn: client.ConsumerChannel{ 448 Balance: initialBalance, 449 Settled: big.NewInt(0), 450 }, 451 } 452 calc := mockAddressProvider{} 453 mockBlockchainProvider := mockBlockchainInfoProvider{} 454 455 cbt := NewConsumerBalanceTracker(bus, &bc, &mcts, &mockconsumerInfoGetter{}, &mockTransactor{}, &mockRegistrationStatusProvider{}, &calc, &mockBlockchainProvider, defaultCfg) 456 457 // Make sure we are not dead locked here. https://github.com/mysteriumnetwork/node/issues/2181 458 cbt.updateGrandTotal(1, identity.FromAddress("0x0000"), big.NewInt(1)) 459 } 460 461 func TestConsumerBalance_GetBalance(t *testing.T) { 462 type fields struct { 463 BCBalance *big.Int 464 BCSettled *big.Int 465 GrandTotalPromised *big.Int 466 } 467 tests := []struct { 468 name string 469 fields fields 470 want *big.Int 471 }{ 472 { 473 name: "handles bc balance underflow", 474 fields: fields{ 475 BCBalance: big.NewInt(0), 476 BCSettled: big.NewInt(0), 477 GrandTotalPromised: big.NewInt(1), 478 }, 479 want: big.NewInt(0), 480 }, 481 { 482 name: "handles grand total underflow", 483 fields: fields{ 484 BCBalance: big.NewInt(0), 485 BCSettled: big.NewInt(1), 486 GrandTotalPromised: big.NewInt(0), 487 }, 488 want: big.NewInt(0), 489 }, 490 { 491 name: "calculates balance correctly", 492 fields: fields{ 493 BCBalance: big.NewInt(3), 494 BCSettled: big.NewInt(1), 495 GrandTotalPromised: big.NewInt(2), 496 }, 497 want: big.NewInt(2), 498 }, 499 } 500 for _, tt := range tests { 501 t.Run(tt.name, func(t *testing.T) { 502 cb := ConsumerBalance{ 503 BCBalance: tt.fields.BCBalance, 504 BCSettled: tt.fields.BCSettled, 505 GrandTotalPromised: tt.fields.GrandTotalPromised, 506 } 507 if got := cb.GetBalance(); got.Cmp(tt.want) != 0 { 508 t.Errorf("ConsumerBalance.GetBalance() = %v, want %v", got, tt.want) 509 } 510 }) 511 } 512 } 513 514 type mockAddressProvider struct { 515 transactor common.Address 516 addrToReturn common.Address 517 channels map[string]common.Address 518 } 519 520 func newMockAddressProvider() *mockAddressProvider { 521 return &mockAddressProvider{channels: make(map[string]common.Address)} 522 } 523 524 func (ma *mockAddressProvider) GetActiveChannelAddress(chainID int64, id common.Address) (common.Address, error) { 525 return ma.addrToReturn, nil 526 } 527 func (ma *mockAddressProvider) GetActiveChannelImplementation(chainID int64) (common.Address, error) { 528 return common.Address{}, nil 529 } 530 func (ma *mockAddressProvider) GetMystAddress(chainID int64) (common.Address, error) { 531 return common.Address{}, nil 532 } 533 func (ma *mockAddressProvider) GetActiveHermes(chainID int64) (common.Address, error) { 534 return common.Address{}, nil 535 } 536 func (ma *mockAddressProvider) GetRegistryAddress(chainID int64) (common.Address, error) { 537 return common.Address{}, nil 538 } 539 func (ma *mockAddressProvider) GetArbitraryChannelAddress(hermes, registry, channel common.Address, id common.Address) (common.Address, error) { 540 return ma.addrToReturn, nil 541 } 542 func (ma *mockAddressProvider) GetChannelImplementationForHermes(chainID int64, hermes common.Address) (common.Address, error) { 543 return common.Address{}, nil 544 } 545 func (ma *mockAddressProvider) GetKnownHermeses(chainID int64) ([]common.Address, error) { 546 return []common.Address{ma.addrToReturn}, nil 547 } 548 func (ma *mockAddressProvider) GetHermesChannelAddress(chainID int64, id, hermesAddr common.Address) (common.Address, error) { 549 channelAddr, _ := ma.channels[fmt.Sprintf("%d-%s", chainID, id)] 550 return channelAddr, nil 551 } 552 553 func (ma *mockAddressProvider) setChannelAddress(chainID int64, id, channelAddr common.Address) { 554 ma.channels[fmt.Sprintf("%d-%s", chainID, id)] = channelAddr 555 } 556 557 var _ blockchainInfoProvider = (*mockBlockchainInfoProvider)(nil) 558 559 type mockBlockchainInfoProvider struct { 560 consumerChannelsHermesMap map[string]client.ConsumersHermes 561 } 562 563 func (p *mockBlockchainInfoProvider) GetConsumerChannelsHermes(chainID int64, channelAddress common.Address) (client.ConsumersHermes, error) { 564 result, ok := p.consumerChannelsHermesMap[p.mapKey(chainID, channelAddress)] 565 if !ok { 566 return client.ConsumersHermes{}, fmt.Errorf("mock consumer channels hermes not found") 567 } 568 569 return result, nil 570 } 571 572 func (p *mockBlockchainInfoProvider) AddConsumerChannelsHermes(chainID int64, channelAddress common.Address, consumerHermes client.ConsumersHermes) { 573 if p.consumerChannelsHermesMap == nil { 574 p.consumerChannelsHermesMap = map[string]client.ConsumersHermes{} 575 } 576 577 p.consumerChannelsHermesMap[p.mapKey(chainID, channelAddress)] = consumerHermes 578 } 579 580 func (p *mockBlockchainInfoProvider) mapKey(chainID int64, channelAddress common.Address) string { 581 return fmt.Sprintf("%d_%s", chainID, channelAddress.String()) 582 }