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

[Tokenomics] Preparation for Global Mint Reimbursement Request #755

Merged
merged 61 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
6738d21
WIP
Olshansk Aug 16, 2024
3e6a146
Merge branch 'main' into inflation
Olshansk Aug 20, 2024
c3a235a
Checkpoint
Olshansk Aug 21, 2024
846bc1d
Finished first version of the documentation
Olshansk Aug 21, 2024
40c56a5
Merge branch 'main' into inflation
Olshansk Aug 21, 2024
98736f3
Revert tokenomic docs
Olshansk Aug 21, 2024
eeabb30
Performed self review
Olshansk Aug 21, 2024
10ef513
Merge with main
Olshansk Aug 22, 2024
0e725a4
Remove everything related to TLMGlobalMintReimbursementRequest
Olshansk Aug 22, 2024
bbe2366
WIP
Olshansk Aug 23, 2024
ad70e70
Fixed the TestProcessTokenLogicModules_TLMBurnEqualsMintValid test
Olshansk Aug 23, 2024
b02ef24
WIP
Olshansk Aug 24, 2024
8663fac
Finished implementing TestProcessTokenLogicModules_TLMBurnEqualsMint_…
Olshansk Aug 25, 2024
085d134
Implemented TestProcessTokenLogicModules_TLMGlobalMint_Valid_MintDist…
Olshansk Aug 25, 2024
1de13ba
Updated comments in e2e/tests/0_settlement.feature
Olshansk Aug 25, 2024
b8dff51
Fixed failing unit test
Olshansk Aug 25, 2024
9231d6f
Merge branch 'main' into issues/732_max_claimable_pokt
Olshansk Aug 25, 2024
f79989a
Merge branch 'main' into issues/732_max_claimable_pokt
Olshansk Aug 26, 2024
4c335e7
update compile proto
Olshansk Aug 26, 2024
e0ea0d5
Merge branch 'main' into issues/732_max_claimable_pokt
Olshansk Aug 28, 2024
ccb4e12
Update proto/poktroll/tokenomics/event.proto
Olshansk Aug 28, 2024
7fce0fc
Update x/tokenomics/keeper/keeper_settle_pending_claims_test.go
Olshansk Aug 28, 2024
f60cc46
WIP comments
Olshansk Aug 28, 2024
6298765
WIP comments
Olshansk Aug 28, 2024
b0266b0
Resolved a few simple comments
Olshansk Aug 28, 2024
754b52c
Apply suggestions from code review
Olshansk Aug 28, 2024
e98ac7a
Apply suggestions from code review
Olshansk Aug 28, 2024
5065264
Apply suggestions from code review
Olshansk Aug 28, 2024
7f098f4
Apply suggestions from code review
Olshansk Aug 28, 2024
f63a8dd
Tending to a partial set of comments
Olshansk Aug 28, 2024
da081e9
Update testutil/testrelayer/relays.go
Olshansk Aug 28, 2024
b32397d
Update tests/integration/tokenomics/relay_mining_difficulty_test.go
Olshansk Aug 28, 2024
02fd89c
Final set of comments
Olshansk Aug 29, 2024
2474df4
Merge with main
Olshansk Aug 31, 2024
73ea7e2
Reply to a few small review comments
Olshansk Sep 3, 2024
b0578be
Empty commit
Olshansk Sep 3, 2024
5d25b3e
Update x/tokenomics/keeper/token_logic_modules.go
Olshansk Sep 4, 2024
7e07852
Update pkg/crypto/rings/cache_test.go
Olshansk Sep 4, 2024
05d78a6
Update testutil/sample/sample.go
Olshansk Sep 4, 2024
939d913
Update x/tokenomics/keeper/token_logic_modules.go
Olshansk Sep 4, 2024
d0a89d8
Update x/tokenomics/keeper/token_logic_modules.go
Olshansk Sep 4, 2024
461d4af
Update x/tokenomics/keeper/token_logic_modules.go
Olshansk Sep 4, 2024
a4d6375
build(deps): bump webpack from 5.89.0 to 5.94.0 in /docusaurus (#775)
dependabot[bot] Sep 4, 2024
88f1d72
[Docs] Minor doc updates across the board (#792)
Olshansk Sep 4, 2024
63573f2
[Tilt] Enable rest service by default in Tilt (#793)
red-0ne Sep 4, 2024
4343be2
Reply to a few small review comments
Olshansk Sep 4, 2024
13af4d7
Merge branch 'main' into issues/732_max_claimable_pokt
Olshansk Sep 4, 2024
4fd00d8
Reply to a few small review comments
Olshansk Sep 4, 2024
262a756
Merge with main
Olshansk Sep 5, 2024
8d5b083
Updated compiled proto file
Olshansk Sep 5, 2024
4bd0aee
Empty commit
Olshansk Sep 5, 2024
7767954
Merge branch 'main' into issues/732_max_claimable_pokt
Olshansk Sep 6, 2024
24479c9
Merge with main
Olshansk Sep 6, 2024
85b57df
Merge branch 'main' into issues/732_max_claimable_pokt
Olshansk Sep 9, 2024
66b6878
Minor tx.pb.go fix
Olshansk Sep 9, 2024
a0dbd57
Empty commit
Olshansk Sep 9, 2024
584215f
Merge branch 'main' into issues/732_max_claimable_pokt
Olshansk Sep 9, 2024
8d04dfd
Added a TODO
Olshansk Sep 9, 2024
d992098
E2E test fix
Olshansk Sep 9, 2024
5795b5e
Increase run-e2e-tests timeout to 30 minutes from 20
Olshansk Sep 9, 2024
8a42835
Another attempt at fixing E2E tests
Olshansk Sep 9, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/main-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
context: .

run-e2e-tests:
timeout-minutes: 20
timeout-minutes: 30
needs: build-push-container
if: contains(github.event.pull_request.labels.*.name, 'devnet-test-e2e')
runs-on: ubuntu-latest
Expand Down
6 changes: 3 additions & 3 deletions api/poktroll/shared/service.pulsar.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

169 changes: 126 additions & 43 deletions api/poktroll/tokenomics/event.pulsar.go

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ accounts:
mnemonic: "involve clean slab term real human green immune valid swing protect talk silent unique cart few ice era right thunder again drop among bounce"
coins:
- 300000000upokt
- name: apptiny
mnemonic: "worry pupil rival such jump pitch flame prosper tattoo eternal round receive cube crowd remove afraid garment brand toy nut guitar toy sausage fragile"
coins:
- 1000000upokt # 1 POKT
- name: supplier1
mnemonic: "cool industry busy tumble funny relax error state height like board wing goat emerge visual idle never unveil announce hill primary okay spatial frog"
coins:
Expand Down Expand Up @@ -176,6 +180,20 @@ genesis:
# `supplier1_stake_config.yaml` so that the stake command causes a state change.
amount: "1000068"
denom: upokt
- address: pokt1ad28jdap2zfanjd7hpkh984yveney6k9a42man
delegatee_gateway_addresses: []
service_configs:
- service:
id: anvil
- service:
id: rest
- service:
id: ollama
stake:
# NB: This value should be exactly 1upokt smaller than the value in
# `supplier1_stake_config.yaml` so that the stake command causes a state change.
amount: "1000068"
denom: upokt
supplier:
supplierList:
- owner_address: pokt19a3t4yunp0dlpfjrp7qwnzwlrzd5fzs2gjaaaj
Expand Down
33 changes: 21 additions & 12 deletions e2e/tests/0_settlement.feature
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,38 @@
# that can be used to clear the state of the chain between tests.

Feature: Tokenomics Namespace

Scenario: Emissions equals burn when a claim is created and a valid proof is submitted and required via threshold
Scenario: Settle the session when a valid claim is within max limits and a valid proof is submitted and required via threshold
# Baseline
Given the user has the pocketd binary installed
# Network preparation
# Network preparation and validation
And an account exists for "supplier1"
And the "supplier" account for "supplier1" is staked
And an account exists for "app1"
And the "application" account for "app1" is staked
And the service "anvil" registered for application "app1" has a compute units per relay of "1"
# Start servicing
# Set proof_requirement_threshold to 9 < num_relays (10) * compute_units_per_relay (1)
# Start servicing relays
# Set proof_requirement_threshold to 19 < num_relays (20) * compute_units_per_relay (1)
# to make sure a proof is required.
And the "proof" module parameters are set as follows
| name | value | type |
| relay_difficulty_target_hash | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff | bytes |
| proof_request_probability | 0.25 | float |
| proof_requirement_threshold | 9 | int64 |
| proof_requirement_threshold | 19 | int64 |
| proof_missing_penalty | 320 | coin |
| proof_submission_fee | 1000000 | coin |
When the supplier "supplier1" has serviced a session with "10" relays for service "anvil" for application "app1"
When the supplier "supplier1" has serviced a session with "20" relays for service "anvil" for application "app1"
# Wait for the Claim & Proof lifecycle
And the user should wait for the "proof" module "CreateClaim" Message to be submitted
And the user should wait for the "proof" module "SubmitProof" Message to be submitted
And the user should wait for the ClaimSettled event with "THRESHOLD" proof requirement to be broadcast
# Validate the results
Then the account balance of "supplier1" should be "420" uPOKT "more" than before
And the "application" stake of "app1" should be "420" uPOKT "less" than before
# Please note that supplier mint is > app burn because of inflation
# TODO_TECHDEBT: Update this test such the the inflation is set and enforce that Mint=Burn
# Then add a separate test that only validates that inflation is enforced correctly
Then the account balance of "supplier1" should be "898" uPOKT "more" than before
And the "application" stake of "app1" should be "840" uPOKT "less" than before

Scenario: Emissions equals burn when a claim is created but a proof is not required
Scenario: Settle the session when a valid claim is create but not required
# Baseline
Given the user has the pocketd binary installed
# Network preparation
Expand All @@ -59,11 +61,18 @@ Feature: Tokenomics Namespace
# No proof should be submitted, don't wait for one.
And the user should wait for the ClaimSettled event with "NOT_REQUIRED" proof requirement to be broadcast
# Validate the results
Then the account balance of "supplier1" should be "420" uPOKT "more" than before
# Please note that supplier mint is > app burn because of inflation
# TODO_TECHDEBT: Update this test such the the inflation is set and enforce that Mint=Burn
Then the account balance of "supplier1" should be "449" uPOKT "more" than before
And the "application" stake of "app1" should be "420" uPOKT "less" than before

# TODO_ADDTEST: Implement the following scenarios
# Scenario: Emissions equals burn when a claim is created and a valid proof is submitted but not required
# Scenario: Supplier revenue shares are properly distributed
# Scenario: TLM Mint=Burn when a valid claim is outside Max Limits
# - Ensure over serviced event is submitted
# Scenario: TLM GlobalMint properly distributes minted rewards to all actors
# - Ensure reimbursement request is submitted
# Scenario: Mint equals burn when a claim is created and a valid proof is submitted but not required
# Scenario: No emissions or burn when a claim is created and an invalid proof is submitted
# Scenario: No emissions or burn when a claim is created and a proof is required but is not submitted
# Scenario: No emissions or burn when no claim is created
1 change: 0 additions & 1 deletion e2e/tests/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,6 @@ func (s *suite) getConfigFileContent(
default:
s.Fatalf("ERROR: unknown actor type %s", actorType)
}
fmt.Println(yaml.NormalizeYAMLIndentation(configContent))
return yaml.NormalizeYAMLIndentation(configContent)
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/crypto/rings/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func newAccount(curve string) account {
var pubkey cryptotypes.PubKey
switch curve {
case "ed25519":
addr, pubkey = sample.AccAddressAndPubKeyEdd2519()
addr, pubkey = sample.AccAddressAndPubKeyEd25519()
case "secp256k1":
addr, pubkey = sample.AccAddressAndPubKey()
}
Expand Down
1 change: 1 addition & 0 deletions pkg/relayer/proxy/relay_verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func (rp *relayerProxy) VerifyRelayRequest(
}).
Msg("verifying relay request session")

// TODO_TECHDEBT(@red-0ne): Optimize this so we don't have to query the session for every relay request.
// Query for the current session to check if relayRequest sessionId matches the current session.
session, err := rp.sessionQuerier.GetSession(
ctx,
Expand Down
17 changes: 16 additions & 1 deletion pkg/relayer/session/sessiontree.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ import (
var _ relayer.SessionTree = (*sessionTree)(nil)

// sessionTree is an implementation of the SessionTree interface.
// TODO_TEST: Add tests to the sessionTree.
// TODO_BETA(@red-0ne): Per the Relay Mining paper, we need to optimistically store
// the number of requests that an application can pay for. This needs to be tracked
// based on the app's stake in the beginning of a session and the number of nodes
// per session. An operator should be able to specify "overservicing_compute_units_limit"
// whereby an upper bound on how much it can overservice an application is set. The
// default value for this should be -1, implying "unlimited".
// Ref discussion: https://github.com/pokt-network/poktroll/pull/755#discussion_r1737287860
type sessionTree struct {
// sessionMu is a mutex used to protect sessionTree operations from concurrent access.
sessionMu *sync.Mutex
Expand Down Expand Up @@ -65,6 +71,15 @@ type sessionTree struct {
// NewSessionTree creates a new sessionTree from a Session and a storePrefix. It also takes a function
// removeFromRelayerSessions that removes the sessionTree from the RelayerSessionsManager.
// It returns an error if the KVStore fails to be created.
//
// TODO_BETA(@red-0ne): When starting a new session, check what the MaxClaimableAmount
// (in uPOKT) by the Supplier as a function of
// (app_stake, compute_units_per_relay_for_service, global_compute_units_to_token_multiplier).
// TODO_CONFIG_NOTE: Whether or not the RelayMiner stop handling requests when the max is reached should be
// configurable by the operator.
// TODO_ERROR_NOTE: If overservicing is set to false, create a new error that the relay is rejected
// specifically because the supplier has reached the max claimable amount, so the caller should relay
// the request to another supplier.
func NewSessionTree(
sessionHeader *sessiontypes.SessionHeader,
supplierOperatorAddress *cosmostypes.AccAddress,
Expand Down
6 changes: 3 additions & 3 deletions proto/poktroll/shared/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ message Service {
// For example, what if we want to request a session for a certain service but with some additional configs that identify it?
string id = 1; // Unique identifier for the service

// TODO_MAINNET: Remove this.
// TODO_BETA: Either remove this or rename it to alias.
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
string name = 2; // (Optional) Semantic human readable name for the service

// The cost of a single relay for this service in terms of compute units.
Expand All @@ -32,7 +32,7 @@ message Service {

// ApplicationServiceConfig holds the service configuration the application stakes for
message ApplicationServiceConfig {
// TODO_MAINNET: Avoid embedding the full Service because we just need the ID.
// TODO_BETA: Avoid embedding the full Service because we just need the ID.
Service service = 1; // The Service for which the application is configured

// TODO_MAINNET: There is an opportunity for applications to advertise the max
Expand All @@ -42,7 +42,7 @@ message ApplicationServiceConfig {

// SupplierServiceConfig holds the service configuration the supplier stakes for
message SupplierServiceConfig {
// TODO_MAINNET: Avoid embedding the full Service because we just need the ID.
// TODO_BETA: Avoid embedding the full Service because we just need the ID.
Service service = 1; // The Service for which the supplier is configured
repeated SupplierEndpoint endpoints = 2; // List of endpoints for the service
repeated ServiceRevenueShare rev_share = 3; // List of revenue share configurations for the service
Expand Down
21 changes: 15 additions & 6 deletions proto/poktroll/tokenomics/event.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ package poktroll.tokenomics;
option go_package = "github.com/pokt-network/poktroll/x/tokenomics/types";
option (gogoproto.stable_marshaler_all) = true;

import "gogoproto/gogo.proto";
import "cosmos/base/v1beta1/coin.proto";
import "gogoproto/gogo.proto";
import "poktroll/proof/types.proto";

enum ClaimExpirationReason {
Expand Down Expand Up @@ -44,10 +44,19 @@ message EventRelayMiningDifficultyUpdated {
uint64 new_num_relays_ema = 5;
}

// EventApplicationOverserviced is emitted when an application has less stake
// than the expected burn.
// EventApplicationOverserviced is emitted when an application has less stake than
// what a supplier is claiming (i.e. amount available for burning is insufficient).
// This means the following will ALWAYS be strictly true: effective_burn < expected_burn.
message EventApplicationOverserviced {
string application_addr = 1;
cosmos.base.v1beta1.Coin expected_burn = 2;
cosmos.base.v1beta1.Coin effective_burn = 3;
}
string supplier_operator_addr = 2;
// Expected burn is the amount the supplier is claiming for work done
// to service the application during the session.
// This is usually the amount in the Claim submitted.
cosmos.base.v1beta1.Coin expected_burn = 3;
// Effective burn is the amount that is actually being paid to the supplier
// for the work done. It is less than the expected burn (claim amount) and
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
// is a function of the relay mining algorithm.
// E.g. The application's stake divided by the number of suppliers in a session.
cosmos.base.v1beta1.Coin effective_burn = 4;
}
46 changes: 25 additions & 21 deletions tests/integration/tokenomics/relay_mining_difficulty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ func TestUpdateRelayMiningDifficulty_NewServiceSeenForTheFirstTime(t *testing.T)
session := getSession(t, integrationApp)
sharedParams := getSharedParams(t, integrationApp)

// Prepare the trie with a single mined relay
trie := prepareSMST(t, sdkCtx, integrationApp, session)
// Prepare the trie with several mined relays
expectedNumRelays := uint64(100)
trie := prepareSMST(t, sdkCtx, integrationApp, session, expectedNumRelays)

// Compute the number of blocks to wait between different events
// TODO_BLOCKER(@bryanchriswhite): See this comment: https://github.com/pokt-network/poktroll/pull/610#discussion_r1645777322
Expand Down Expand Up @@ -118,8 +119,8 @@ func TestUpdateRelayMiningDifficulty_NewServiceSeenForTheFirstTime(t *testing.T)
require.Equal(t, prooftypes.DefaultRelayDifficultyTargetHashHex, relayMiningEvent.NewTargetHashHexEncoded)

// The previous EMA is the same as the current one if the service is new
require.Equal(t, uint64(1), relayMiningEvent.PrevNumRelaysEma)
require.Equal(t, uint64(1), relayMiningEvent.NewNumRelaysEma)
require.Equal(t, expectedNumRelays, relayMiningEvent.PrevNumRelaysEma)
require.Equal(t, expectedNumRelays, relayMiningEvent.NewNumRelaysEma)
}

func UpdateRelayMiningDifficulty_UpdatingMultipleServicesAtOnce(t *testing.T) {}
Expand Down Expand Up @@ -164,11 +165,12 @@ func getSession(t *testing.T, integrationApp *testutil.App) *sessiontypes.Sessio
return getSessionRes.Session
}

// prepareSMST prepares an SMST with a single mined relay for the given session.
// prepareSMST prepares an SMST with the given number of mined relays.
func prepareSMST(
t *testing.T, ctx context.Context,
integrationApp *testutil.App,
session *sessiontypes.Session,
numRelays uint64,
) *smt.SMST {
t.Helper()

Expand All @@ -178,23 +180,25 @@ func prepareSMST(
kvStore, err := pebble.NewKVStore("")
require.NoError(t, err)

// NB: A signed mined relay is a MinedRelay type with the appropriate
// payload, signatures and metadata populated.
//
// It does not (as of writing) adhere to the actual on-chain difficulty (i.e.
// hash check) of the test service surrounding the scope of this test.
minedRelay := testrelayer.NewSignedMinedRelay(t, ctx,
session,
integrationApp.DefaultApplication.Address,
integrationApp.DefaultSupplier.OperatorAddress,
integrationApp.DefaultSupplierKeyringKeyringUid,
integrationApp.GetKeyRing(),
integrationApp.GetRingClient(),
)

trie := smt.NewSparseMerkleSumTrie(kvStore, protocol.NewTrieHasher(), smt.WithValueHasher(nil))
err = trie.Update(minedRelay.Hash, minedRelay.Bytes, 1)
require.NoError(t, err)

for i := uint64(0); i < numRelays; i++ {
// DEV_NOTE: A signed mined relay is a MinedRelay type with the appropriate
// payload, signatures and metadata populated.
// It does not (as of writing) adhere to the actual on-chain difficulty (i.e.
// hash check) of the test service surrounding the scope of this test.
minedRelay := testrelayer.NewSignedMinedRelay(t, ctx,
session,
integrationApp.DefaultApplication.Address,
integrationApp.DefaultSupplier.OperatorAddress,
integrationApp.DefaultSupplierKeyringKeyringUid,
integrationApp.GetKeyRing(),
integrationApp.GetRingClient(),
)

err = trie.Update(minedRelay.Hash, minedRelay.Bytes, 1)
require.NoError(t, err)
}

return trie
}
Expand Down
2 changes: 1 addition & 1 deletion testutil/integration/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -517,8 +517,8 @@ func NewCompleteIntegrationApp(t *testing.T) *App {
defaultService := sharedtypes.Service{
Id: "svc1",
Name: "svcName1",
OwnerAddress: sample.AccAddress(),
ComputeUnitsPerRelay: 1,
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
OwnerAddress: sample.AccAddress(),
}
serviceKeeper.SetService(integrationApp.sdkCtx, defaultService)
integrationApp.DefaultService = &defaultService
Expand Down
16 changes: 16 additions & 0 deletions testutil/keeper/tokenomics.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ func TokenomicsKeeperWithActorAddrs(t testing.TB) (
mockBankKeeper.EXPECT().
SendCoinsFromModuleToAccount(gomock.Any(), tokenomicstypes.ModuleName, gomock.Any(), gomock.Any()).
AnyTimes()
mockBankKeeper.EXPECT().
SendCoinsFromModuleToModule(gomock.Any(), tokenomicstypes.ModuleName, suppliertypes.ModuleName, gomock.Any()).
AnyTimes()

// Mock the account keeper
mockAccountKeeper := mocks.NewMockAccountKeeper(ctrl)
Expand Down Expand Up @@ -459,3 +462,16 @@ func WithService(service sharedtypes.Service) TokenomicsModuleKeepersOpt {
return ctx
}
}

func WithProposerAddr(addr string) TokenomicsModuleKeepersOpt {
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
return func(ctx context.Context, keepers *TokenomicsModuleKeepers) context.Context {
valAddr, err := cosmostypes.ValAddressFromBech32(addr)
if err != nil {
panic(err)
}
consensusAddr := cosmostypes.ConsAddress(valAddr)
sdkCtx := sdk.UnwrapSDKContext(ctx)
sdkCtx = sdkCtx.WithProposer(consensusAddr)
return sdkCtx
}
}
Loading
Loading