Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: throttle with retries provider changes #1230

Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
7691bf5
wip, tests not fixed yet
shaspitz Jun 20, 2023
a10a239
rm packet query on provider
shaspitz Aug 22, 2023
8a557b3
rm unneeded UTs
shaspitz Aug 22, 2023
196ce38
rm tests from relay_test
shaspitz Aug 22, 2023
0f27c31
rm query and more tests
shaspitz Aug 22, 2023
cf09f5f
rm more tests
shaspitz Aug 22, 2023
87ad0f4
Merge branch 'main' into shawn/throttle-with-retries-provider-changes
shaspitz Aug 22, 2023
7e6264f
builds again and rm debug tests
shaspitz Aug 22, 2023
5e4b845
lint
shaspitz Aug 22, 2023
8350956
fix handling of slash packet and integration test
shaspitz Aug 22, 2023
6d20dd1
Fix TestMultiConsumerSlashPacketThrottling
shaspitz Aug 23, 2023
461878c
fix two more slashing integration tests
shaspitz Aug 23, 2023
56242a6
Update TestSlashRetries, cleanup neededc
shaspitz Aug 23, 2023
1d963fa
cleaned up TestSlashRetries
shaspitz Aug 23, 2023
e8acd9e
UT for TestOnRecvDowntimeSlashPacket
shaspitz Aug 23, 2023
8ed33f3
cleans
shaspitz Aug 24, 2023
ecac6a4
use helper in throttle test
shaspitz Aug 24, 2023
f6d4650
lintz
shaspitz Aug 24, 2023
956e595
Revert "rm packet query on provider"
shaspitz Aug 24, 2023
db8dc1b
cmd file too
shaspitz Aug 24, 2023
78a8269
fully restore query
shaspitz Aug 24, 2023
73db33b
Merge branch 'main' into shawn/throttle-with-retries-provider-changes
shaspitz Aug 24, 2023
5bfccc3
Revert "Merge branch 'main' into shawn/throttle-with-retries-provider…
shaspitz Aug 24, 2023
5196394
make e2e test pass, with todos
shaspitz Aug 25, 2023
37e0e93
clean
shaspitz Aug 25, 2023
b1cb354
Revert "Revert "Merge branch 'main' into shawn/throttle-with-retries-…
shaspitz Aug 25, 2023
599854a
lint
shaspitz Aug 25, 2023
8945156
Update CHANGELOG.md
shaspitz Aug 25, 2023
0544fd3
slightly longer buffer
shaspitz Aug 25, 2023
d8f5690
merge main
shaspitz Aug 25, 2023
f91cb70
build(deps): bump actions/checkout from 3 to 4 (#1257)
dependabot[bot] Sep 5, 2023
aca8362
build(deps)!: bump github.com/cosmos/ibc-go/v7 from 7.2.0 to 7.3.0 (#…
dependabot[bot] Sep 5, 2023
66adc8a
build(deps): bump github.com/cosmos/cosmos-sdk from 0.47.4 to 0.47.5 …
dependabot[bot] Sep 5, 2023
cc9064d
chore: Separate semver (#1217)
shaspitz Sep 5, 2023
6da7fef
docs: cleanup changelog (#1260)
mpoke Sep 5, 2023
840d290
fix!: validate MsgTransfer before calling Transfer() (#1244)
mpoke Sep 5, 2023
8ec7bc5
docs: Create adr-012-separate-releasing.md (#1229)
shaspitz Sep 5, 2023
b152c03
fix: remove addr validation for provider fee pool addr param (#1262)
NeverHappened Sep 6, 2023
6bdfff9
Merge branch 'main' into shawn/throttle-with-retries-provider-changes
shaspitz Sep 6, 2023
3b27006
Revert "Merge branch 'main' into shawn/throttle-with-retries-provider…
shaspitz Sep 6, 2023
afa32f4
Merge branch 'feat/throttle-complete-attempt-2' into shawn/throttle-w…
shaspitz Sep 6, 2023
6ee88e2
fmt
shaspitz Sep 6, 2023
1ed2f56
Update steps_downtime.go
shaspitz Sep 14, 2023
fcf0b92
Update tests/e2e/steps_downtime.go
shaspitz Sep 14, 2023
53c02ea
waittime instead of sleep
shaspitz Sep 14, 2023
e9d745d
Update x/ccv/provider/client/cli/query.go
shaspitz Sep 14, 2023
b685958
Update x/ccv/provider/client/cli/query.go
shaspitz Sep 14, 2023
1ceddcb
rm throttled packets from query
shaspitz Sep 14, 2023
e228953
rm provider query
shaspitz Sep 14, 2023
7d3dd64
whoopsies forgot to rm some boilerplate
shaspitz Sep 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions proto/interchain_security/ccv/provider/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@
"/interchain_security/ccv/provider/throttle_state";
}

// QueryThrottledConsumerPacketData returns a list of pending packet data
// instances (slash packet and vsc matured) for a single consumer chain
// [DEPRECATED] Returns an empty set.
rpc QueryThrottledConsumerPacketData(QueryThrottledConsumerPacketDataRequest)
returns (QueryThrottledConsumerPacketDataResponse) {
option (google.api.http).get =
Expand All @@ -86,7 +85,7 @@
message QueryConsumerGenesisRequest { string chain_id = 1; }

message QueryConsumerGenesisResponse {
interchain_security.ccv.v1.GenesisState genesis_state = 1

Check failure on line 88 in proto/interchain_security/ccv/provider/v1/query.proto

View workflow job for this annotation

GitHub Actions / break-check

Field "1" on message "QueryConsumerGenesisResponse" changed type from "interchain_security.ccv.consumer.v1.GenesisState" to "interchain_security.ccv.v1.GenesisState".
[ (gogoproto.nullable) = false ];
}

Expand Down
37 changes: 14 additions & 23 deletions tests/e2e/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -1935,50 +1935,41 @@ func (tr TestRun) assignConsumerPubKey(action assignConsumerPubKeyAction, verbos
tr.waitBlocks(chainID("provi"), 2, 30*time.Second)
}

// slashThrottleDequeue polls slash queue sizes until nextQueueSize is achieved
type slashThrottleDequeue struct {
chain chainID
currentQueueSize int
nextQueueSize int
// slashMeterReplenishmentAction polls the slash meter on provider until value is achieved
type slashMeterReplenishmentAction struct {
targetValue int64
// panic if timeout is exceeded
timeout time.Duration
}

func (tr TestRun) waitForSlashThrottleDequeue(
action slashThrottleDequeue,
func (tr TestRun) waitForSlashMeterReplenishment(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The slash packet queue size of the provider is no longer relevant, but the slash meter value is relevant. I've replaced this action to wait on slash meter being replenished

action slashMeterReplenishmentAction,
verbose bool,
) {
timeout := time.Now().Add(action.timeout)
initialGlobalQueueSize := int(tr.getGlobalSlashQueueSize())
initialSlashMeter := tr.getSlashMeter()

if initialGlobalQueueSize != action.currentQueueSize {
panic(fmt.Sprintf("wrong initial queue size: %d - expected global queue: %d\n", initialGlobalQueueSize, action.currentQueueSize))
if initialSlashMeter >= 0 {
panic(fmt.Sprintf("No need to wait for slash meter replenishment, current value: %d", initialSlashMeter))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a bad idea to panic on? It makes the tests flakey, because if they happen to run slowly, the slash meter might be already-full, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I don't see a good way to decouple the logic of the slash meter away from time. Any test for throttling must have a pretty good and mostly deterministic control over chain time

}

for {
globalQueueSize := int(tr.getGlobalSlashQueueSize())
chainQueueSize := int(tr.getConsumerChainPacketQueueSize(action.chain))
slashMeter := tr.getSlashMeter()
if verbose {
fmt.Printf("waiting for packed queue size to reach: %d - current: %d\n", action.nextQueueSize, globalQueueSize)
fmt.Printf("waiting for slash meter to be replenished, current value: %d\n", slashMeter)
}

// check if global queue size is equal to chain queue size
if globalQueueSize == chainQueueSize && globalQueueSize == action.nextQueueSize { //nolint:gocritic // this is the comparison that we want here.
// check if meter has reached target value
if slashMeter >= action.targetValue {
break
}

if time.Now().After(timeout) {
panic(fmt.Sprintf("\n\n\nwaitForSlashThrottleDequeuemethod has timed out after: %s\n\n", action.timeout))
panic(fmt.Sprintf("\n\nwaitForSlashMeterReplenishment has timed out after: %s\n\n", action.timeout))
}

time.Sleep(500 * time.Millisecond)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we replace this with tr.waitBlocks/tr.waitTime?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be waitTime from my understanding of the slashMeter (it fills by timestamp, not by blockcount?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch 53c02ea

}
// wair for 2 blocks to be created
// allowing the jailing to be incorporated into voting power
tr.waitBlocks(action.chain, 2, time.Minute)
}

func uintPointer(i uint) *uint {
return &i
}

// GetPathNameForGorelayer returns the name of the path between two given chains used by Gorelayer.
Expand Down
4 changes: 2 additions & 2 deletions tests/e2e/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ func (tr *TestRun) runStep(step Step, verbose bool) {
tr.registerRepresentative(action, verbose)
case assignConsumerPubKeyAction:
tr.assignConsumerPubKey(action, verbose)
case slashThrottleDequeue:
tr.waitForSlashThrottleDequeue(action, verbose)
case slashMeterReplenishmentAction:
tr.waitForSlashMeterReplenishment(action, verbose)
case startRelayerAction:
tr.startRelayer(action, verbose)
case registerConsumerRewardDenomAction:
Expand Down
40 changes: 5 additions & 35 deletions tests/e2e/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,19 +166,6 @@ func (tr TestRun) getChainState(chain chainID, modelState ChainState) ChainState
chainState.ProviderKeys = &providerKeys
}

if modelState.GlobalSlashQueueSize != nil {
globalQueueSize := tr.getGlobalSlashQueueSize()
chainState.GlobalSlashQueueSize = &globalQueueSize
}

if modelState.ConsumerChainQueueSizes != nil {
consumerChainQueueSizes := map[chainID]uint{}
for c := range *modelState.ConsumerChainQueueSizes {
consumerChainQueueSizes[c] = tr.getConsumerChainPacketQueueSize(c)
}
chainState.ConsumerChainQueueSizes = &consumerChainQueueSizes
}

if modelState.RegisteredConsumerRewardDenoms != nil {
registeredConsumerRewardDenoms := tr.getRegisteredConsumerRewardDenoms(chain)
chainState.RegisteredConsumerRewardDenoms = &registeredConsumerRewardDenoms
Expand Down Expand Up @@ -657,9 +644,10 @@ func (tr TestRun) getProviderAddressFromConsumer(consumerChain chainID, validato
return addr
}

func (tr TestRun) getGlobalSlashQueueSize() uint {
func (tr TestRun) getSlashMeter() int64 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this take the chain as an argument instead of always using provi?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo no because the slash meter is only relevant to the provider. If we ever plan to have multiple providers in these tests then that's a different story, but I don't see that happening

//#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments.
cmd := exec.Command("docker", "exec", tr.containerConfig.instanceName, tr.chainConfigs[chainID("provi")].binaryName,
cmd := exec.Command("docker", "exec",
tr.containerConfig.instanceName, tr.chainConfigs[chainID("provi")].binaryName,

"query", "provider", "throttle-state",
`--node`, tr.getQueryNode(chainID("provi")),
Expand All @@ -670,26 +658,8 @@ func (tr TestRun) getGlobalSlashQueueSize() uint {
log.Fatal(err, "\n", string(bz))
}

packets := gjson.Get(string(bz), "packets").Array()
return uint(len(packets))
}

func (tr TestRun) getConsumerChainPacketQueueSize(consumerChain chainID) uint {
//#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments.
cmd := exec.Command("docker", "exec", tr.containerConfig.instanceName, tr.chainConfigs[chainID("provi")].binaryName,

"query", "provider", "throttled-consumer-packet-data",
string(consumerChain),
`--node`, tr.getQueryNode(chainID("provi")),
`-o`, `json`,
)
bz, err := cmd.CombinedOutput()
if err != nil {
log.Fatal(err, "\n", string(bz))
}

size := gjson.Get(string(bz), "size").Uint()
return uint(size)
slashMeter := gjson.Get(string(bz), "slash_meter")
return slashMeter.Int()
}

func (tr TestRun) getRegisteredConsumerRewardDenoms(chain chainID) []string {
Expand Down
66 changes: 11 additions & 55 deletions tests/e2e/steps_downtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ func stepsThrottledDowntime(consumerName string) []Step {
validator: validatorID("bob"),
},
state: State{
// slash packet queued on consumer, but powers not affected on either chain yet
// slash packet queued for bob on consumer, but powers not affected on either chain yet
chainID("provi"): ChainState{
ValPowers: &map[validatorID]uint{
validatorID("alice"): 511,
Expand Down Expand Up @@ -312,11 +312,6 @@ func stepsThrottledDowntime(consumerName string) []Step {
validatorID("bob"): 0, // bob is jailed
validatorID("carol"): 500,
},
// no provider throttling engaged yet
GlobalSlashQueueSize: uintPointer(0),
ConsumerChainQueueSizes: &map[chainID]uint{
chainID(consumerName): uint(0),
},
},
chainID(consumerName): ChainState{
// VSC packet applying jailing is not yet relayed to consumer
Expand All @@ -328,13 +323,13 @@ func stepsThrottledDowntime(consumerName string) []Step {
},
},
},
// Invoke carol downtime slash on consumer
{
action: downtimeSlashAction{
chain: chainID(consumerName),
validator: validatorID("carol"),
},
state: State{
// powers not affected on either chain yet
chainID("provi"): ChainState{
ValPowers: &map[validatorID]uint{
validatorID("alice"): 511,
Expand All @@ -343,10 +338,9 @@ func stepsThrottledDowntime(consumerName string) []Step {
},
},
chainID(consumerName): ChainState{
// VSC packet applying jailing is not yet relayed to consumer
ValPowers: &map[validatorID]uint{
validatorID("alice"): 511,
validatorID("bob"): 500,
validatorID("bob"): 500, // VSC packet applying bob jailing is not yet relayed to consumer
shaspitz marked this conversation as resolved.
Show resolved Hide resolved
validatorID("carol"): 500,
},
},
Expand All @@ -364,27 +358,24 @@ func stepsThrottledDowntime(consumerName string) []Step {
ValPowers: &map[validatorID]uint{
validatorID("alice"): 511,
validatorID("bob"): 0,
validatorID("carol"): 500, // not slashed due to throttling
},
GlobalSlashQueueSize: uintPointer(1), // carol's slash request is throttled
ConsumerChainQueueSizes: &map[chainID]uint{
chainID(consumerName): uint(1),
validatorID("carol"): 500, // slash packet for carol recv by provider, carol not slashed due to throttling
},
},
chainID(consumerName): ChainState{
ValPowers: &map[validatorID]uint{
validatorID("alice"): 511,
validatorID("bob"): 0,
validatorID("bob"): 0, // VSC packet applying bob jailing is also relayed and recv by consumer
validatorID("carol"): 500,
},
},
},
},
// TODO(Shawn): Improve this test to have the consumer retry it's downtime slash, and to assert queue size on consumer.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note this TODO, the e2e test ends without retrying the slash packet. This will be addressed in a separate PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm, we are removing the Queue size from the chain state, despite this TODO, right? It reads to me as if that would reintroduce it, but maybe I'm missing context

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, we are removing the queues from the provider in this PR. The queues exist on the consumers as of #1024, so incoming e2e test will assert queue size on consumer

// See https://github.com/cosmos/interchain-security/issues/1103 and https://github.com/cosmos/interchain-security/issues/1233
{
action: slashThrottleDequeue{
chain: chainID(consumerName),
currentQueueSize: 1,
nextQueueSize: 0,
action: slashMeterReplenishmentAction{
targetValue: 0, // We just want slash meter to be non-negative

// Slash meter replenish fraction is set to 10%, replenish period is 20 seconds, see config.go
// Meter is initially at 10%, decremented to -23% from bob being jailed. It'll then take three replenishments
// for meter to become positive again. 3*20 = 60 seconds + buffer = 80 seconds
Expand All @@ -395,11 +386,7 @@ func stepsThrottledDowntime(consumerName string) []Step {
ValPowers: &map[validatorID]uint{
validatorID("alice"): 511,
validatorID("bob"): 0,
validatorID("carol"): 0, // Carol is jailed upon packet being handled on provider
},
GlobalSlashQueueSize: uintPointer(0), // slash packets dequeued
ConsumerChainQueueSizes: &map[chainID]uint{
chainID(consumerName): 0,
validatorID("carol"): 500, // Carol still not slashed, packet must be retried
},
},
chainID(consumerName): ChainState{
Expand All @@ -412,36 +399,5 @@ func stepsThrottledDowntime(consumerName string) []Step {
},
},
},
// A block is incremented each action, hence why VSC is committed on provider,
// and can now be relayed as packet to consumer
{
action: relayPacketsAction{
chainA: chainID("provi"),
chainB: chainID(consumerName),
port: "provider",
channel: 0,
},
state: State{
chainID("provi"): ChainState{
ValPowers: &map[validatorID]uint{
validatorID("alice"): 511,
validatorID("bob"): 0,
validatorID("carol"): 0,
},
GlobalSlashQueueSize: uintPointer(0),
ConsumerChainQueueSizes: &map[chainID]uint{
chainID(consumerName): 0,
},
},
chainID(consumerName): ChainState{
ValPowers: &map[validatorID]uint{
validatorID("alice"): 511,
// throttled update gets to consumer
validatorID("bob"): 0,
validatorID("carol"): 0,
},
},
},
},
}
}
71 changes: 35 additions & 36 deletions tests/integration/common.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package integration

import (
"bytes"
"fmt"
"time"

Expand Down Expand Up @@ -414,58 +415,41 @@ func (suite *CCVTestSuite) commitConsumerPacket(ctx sdk.Context, packetData ccv.
func (s *CCVTestSuite) constructSlashPacketFromConsumer(bundle icstestingutils.ConsumerBundle,
tmVal tmtypes.Validator, infractionType stakingtypes.Infraction, ibcSeqNum uint64,
) channeltypes.Packet {
packet, _ := s.constructSlashPacketFromConsumerWithData(bundle, tmVal, infractionType, ibcSeqNum)
return packet
}

func (s *CCVTestSuite) constructSlashPacketFromConsumerWithData(bundle icstestingutils.ConsumerBundle,
tmVal tmtypes.Validator, infractionType stakingtypes.Infraction, ibcSeqNum uint64,
) (channeltypes.Packet, ccv.SlashPacketData) {
valsetUpdateId := bundle.GetKeeper().GetHeightValsetUpdateID(
bundle.GetCtx(), uint64(bundle.GetCtx().BlockHeight()))

data := ccv.ConsumerPacketData{
Type: ccv.SlashPacket,
Data: &ccv.ConsumerPacketData_SlashPacketData{
SlashPacketData: &ccv.SlashPacketData{
Validator: abci.Validator{
Address: tmVal.Address,
Power: tmVal.VotingPower,
},
ValsetUpdateId: valsetUpdateId,
Infraction: infractionType,
},
spdData := ccv.SlashPacketData{
Validator: abci.Validator{
Address: tmVal.Address,
Power: tmVal.VotingPower,
},
ValsetUpdateId: valsetUpdateId,
Infraction: infractionType,
}

return channeltypes.NewPacket(data.GetBytes(),
ibcSeqNum,
ccv.ConsumerPortID, // Src port
bundle.Path.EndpointA.ChannelID, // Src channel
ccv.ProviderPortID, // Dst port
bundle.Path.EndpointB.ChannelID, // Dst channel
clienttypes.Height{},
uint64(bundle.GetCtx().BlockTime().Add(ccv.DefaultCCVTimeoutPeriod).UnixNano()),
)
}

// constructVSCMaturedPacketFromConsumer constructs an IBC packet embedding
// VSC Matured packet data to be sent from consumer to provider
func (s *CCVTestSuite) constructVSCMaturedPacketFromConsumer(bundle icstestingutils.ConsumerBundle,
ibcSeqNum uint64,
) channeltypes.Packet {
valsetUpdateId := bundle.GetKeeper().GetHeightValsetUpdateID(
bundle.GetCtx(), uint64(bundle.GetCtx().BlockHeight()))

data := ccv.ConsumerPacketData{
Type: ccv.VscMaturedPacket,
Data: &ccv.ConsumerPacketData_VscMaturedPacketData{
VscMaturedPacketData: &ccv.VSCMaturedPacketData{ValsetUpdateId: valsetUpdateId},
cpdData := ccv.ConsumerPacketData{
Type: ccv.SlashPacket,
Data: &ccv.ConsumerPacketData_SlashPacketData{
SlashPacketData: &spdData,
},
}

return channeltypes.NewPacket(data.GetBytes(),
return channeltypes.NewPacket(cpdData.GetBytes(),
ibcSeqNum,
ccv.ConsumerPortID, // Src port
bundle.Path.EndpointA.ChannelID, // Src channel
ccv.ProviderPortID, // Dst port
bundle.Path.EndpointB.ChannelID, // Dst channel
clienttypes.Height{},
uint64(bundle.GetCtx().BlockTime().Add(ccv.DefaultCCVTimeoutPeriod).UnixNano()),
)
), spdData
}

// incrementTime increments the overall time by jumpPeriod
Expand Down Expand Up @@ -615,3 +599,18 @@ func (s *CCVTestSuite) setupValidatorPowers() {
}
s.Require().Equal(int64(4000), stakingKeeper.GetLastTotalPower(s.providerCtx()).Int64())
}

// mustGetStakingValFromTmVal returns the staking validator from the current state of the staking keeper,
// corresponding to a given tendermint validator. Note this func will fail the test if the validator is not found.
func (s *CCVTestSuite) mustGetStakingValFromTmVal(tmVal tmtypes.Validator) (stakingVal stakingtypes.Validator) {
vals := s.providerApp.GetTestStakingKeeper().GetAllValidators(s.providerCtx())
for i, val := range vals {
consAddr, err := val.GetConsAddr()
s.Require().NoError(err)
if bytes.Equal(consAddr.Bytes(), tmVal.Address.Bytes()) {
stakingVal = vals[i]
}
}
s.Require().NotZero(stakingVal)
return stakingVal
}
Loading
Loading