Skip to content

Commit

Permalink
imp: use bytes in wasm contract api instead of wrapped types (#5154)
Browse files Browse the repository at this point in the history
Co-authored-by: srdtrk <srdtrk@hotmail.com>
Co-authored-by: Damian Nolan <damiannolan@gmail.com>
  • Loading branch information
3 people committed Dec 5, 2023
1 parent fe77a0f commit afd5b52
Show file tree
Hide file tree
Showing 21 changed files with 160 additions and 96 deletions.
7 changes: 4 additions & 3 deletions docs/docs/03-light-clients/04-wasm/07-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ The `08-wasm` light client proxy performs calls to the Wasm light client via the

## `InstantiateMessage`

This is the message sent to the contract's `instantiate` entry point. It contains the client and consensus state provided in [`MsgCreateClient`](https://github.com/cosmos/ibc-go/blob/v7.2.0/proto/ibc/core/client/v1/tx.proto#L25-L37).
This is the message sent to the contract's `instantiate` entry point. It contains the bytes of the protobuf-encoded client and consensus states of the underlying light client, both provided in [`MsgCreateClient`](https://github.com/cosmos/ibc-go/blob/v7.2.0/proto/ibc/core/client/v1/tx.proto#L25-L37). Please note that the bytes contained within the JSON message are represented as base64-encoded strings.

```go
type InstantiateMessage struct {
ClientState *ClientState `json:"client_state"`
ConsensusState *ConsensusState `json:"consensus_state"`
ClientState []byte `json:"client_state"`
ConsensusState []byte `json:"consensus_state"`
Checksum []byte `json:"checksum"
}
```

Expand Down
Binary file removed e2e/tests/wasm/contracts/ics10_grandpa_cw.wasm
Binary file not shown.
Binary file not shown.
Binary file modified e2e/tests/wasm/contracts/ics10_grandpa_cw_expiry.wasm.gz
Binary file not shown.
8 changes: 4 additions & 4 deletions e2e/tests/wasm/grandpa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func (s *GrandpaTestSuite) TestMsgTransfer_Succeeds_GrandpaContract() {

cosmosWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount)

file, err := os.Open("contracts/ics10_grandpa_cw.wasm")
file, err := os.Open("contracts/ics10_grandpa_cw.wasm.gz")
s.Require().NoError(err)

checksum := s.PushNewWasmClientProposal(ctx, cosmosChain, cosmosWallet, file)
Expand Down Expand Up @@ -241,7 +241,7 @@ func (s *GrandpaTestSuite) TestMsgTransfer_TimesOut_GrandpaContract() {

cosmosWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount)

file, err := os.Open("contracts/ics10_grandpa_cw.wasm")
file, err := os.Open("contracts/ics10_grandpa_cw.wasm.gz")
s.Require().NoError(err)

checksum := s.PushNewWasmClientProposal(ctx, cosmosChain, cosmosWallet, file)
Expand Down Expand Up @@ -355,7 +355,7 @@ func (s *GrandpaTestSuite) TestMsgMigrateContract_Success_GrandpaContract() {

cosmosWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount)

file, err := os.Open("contracts/ics10_grandpa_cw.wasm")
file, err := os.Open("contracts/ics10_grandpa_cw.wasm.gz")
s.Require().NoError(err)

checksum := s.PushNewWasmClientProposal(ctx, cosmosChain, cosmosWallet, file)
Expand Down Expand Up @@ -442,7 +442,7 @@ func (s *GrandpaTestSuite) TestMsgMigrateContract_ContractError_GrandpaContract(

cosmosWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount)

file, err := os.Open("contracts/ics10_grandpa_cw.wasm")
file, err := os.Open("contracts/ics10_grandpa_cw.wasm.gz")
s.Require().NoError(err)
checksum := s.PushNewWasmClientProposal(ctx, cosmosChain, cosmosWallet, file)

Expand Down
11 changes: 9 additions & 2 deletions modules/light-clients/08-wasm/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,15 @@ func (suite *KeeperTestSuite) setupWasmWithMockVM() (ibctesting.TestingApp, map[
err := json.Unmarshal(initMsg, &payload)
suite.Require().NoError(err)

store.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), payload.ClientState))
store.Set(host.ConsensusStateKey(payload.ClientState.LatestHeight), clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), payload.ConsensusState))
wrappedClientState := clienttypes.MustUnmarshalClientState(suite.chainA.App.AppCodec(), payload.ClientState)

clientState := types.NewClientState(payload.ClientState, payload.Checksum, wrappedClientState.GetLatestHeight().(clienttypes.Height))
clientStateBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), clientState)
store.Set(host.ClientStateKey(), clientStateBz)

consensusState := types.NewConsensusState(payload.ConsensusState)
consensusStateBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), consensusState)
store.Set(host.ConsensusStateKey(clientState.GetLatestHeight()), consensusStateBz)

resp, err := json.Marshal(types.EmptyResult{})
suite.Require().NoError(err)
Expand Down
5 changes: 3 additions & 2 deletions modules/light-clients/08-wasm/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,9 @@ func (suite *KeeperTestSuite) TestMsgMigrateContract() {

suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) {
// the checksum written in the client state will later be overwritten by the message server.
expClientState = types.NewClientState([]byte{1}, []byte("invalid checksum"), clienttypes.NewHeight(2000, 2))
store.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), expClientState))
expClientStateBz := wasmtesting.CreateMockClientStateBz(suite.chainA.App.AppCodec(), []byte("invalid checksum"))
expClientState = clienttypes.MustUnmarshalClientState(suite.chainA.App.AppCodec(), expClientStateBz).(*types.ClientState)
store.Set(host.ClientStateKey(), expClientStateBz)

data, err := json.Marshal(types.EmptyResult{})
suite.Require().NoError(err)
Expand Down
43 changes: 32 additions & 11 deletions modules/light-clients/08-wasm/testing/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,54 @@ package testing

import (
"errors"
"time"

"github.com/cosmos/cosmos-sdk/codec"

"github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types"
clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
commitmenttypes "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types"
ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint"
testing "github.com/cosmos/ibc-go/v8/testing"
)

var (
// Represents the code of the wasm contract used in the tests with a mock vm.
WasmMagicNumber = []byte("\x00\x61\x73\x6D")
Code = append(WasmMagicNumber, []byte("0123456780123456780123456780")...)
contractClientState = []byte{1}
contractConsensusState = []byte{2}
MockClientStateBz = []byte("client-state-data")
MockConsensusStateBz = []byte("consensus-state-data")
MockValidProofBz = []byte("valid proof")
MockInvalidProofBz = []byte("invalid proof")
MockUpgradedClientStateProofBz = []byte("upgraded client state proof")
MockUpgradedConsensusStateProofBz = []byte("upgraded consensus state proof")
WasmMagicNumber = []byte("\x00\x61\x73\x6D")
Code = append(WasmMagicNumber, []byte("0123456780123456780123456780")...)
MockClientStateBz = []byte("client-state-data")
MockConsensusStateBz = []byte("consensus-state-data")
MockTendermitClientState = CreateMockTendermintClientState(clienttypes.NewHeight(1, 10))
MockTendermintClientHeader = &ibctm.Header{}
MockTendermintClientMisbehaviour = ibctm.NewMisbehaviour("client-id", MockTendermintClientHeader, MockTendermintClientHeader)
MockTendermintClientConsensusState = ibctm.NewConsensusState(time.Now(), commitmenttypes.NewMerkleRoot([]byte("hash")), []byte("nextValsHash"))
MockValidProofBz = []byte("valid proof")
MockInvalidProofBz = []byte("invalid proof")
MockUpgradedClientStateProofBz = []byte("upgraded client state proof")
MockUpgradedConsensusStateProofBz = []byte("upgraded consensus state proof")

ErrMockContract = errors.New("mock contract error")
ErrMockVM = errors.New("mock vm error")
)

// CreateMockTendermintClientState returns a valid Tendermint client state for use in tests.
func CreateMockTendermintClientState(height clienttypes.Height) *ibctm.ClientState {
return ibctm.NewClientState(
"chain-id",
ibctm.DefaultTrustLevel,
testing.TrustingPeriod,
testing.UnbondingPeriod,
testing.MaxClockDrift,
height,
commitmenttypes.GetSDKSpecs(),
testing.UpgradePath,
)
}

// CreateMockClientStateBz returns valid client state bytes for use in tests.
func CreateMockClientStateBz(cdc codec.BinaryCodec, checksum types.Checksum) []byte {
mockClientSate := types.NewClientState([]byte{1}, checksum, clienttypes.NewHeight(2000, 2))
wrappedClientStateBz := clienttypes.MustMarshalClientState(cdc, MockTendermitClientState)
mockClientSate := types.NewClientState(wrappedClientStateBz, checksum, MockTendermitClientState.GetLatestHeight().(clienttypes.Height))
return clienttypes.MustMarshalClientState(cdc, mockClientSate)
}

Expand Down
7 changes: 5 additions & 2 deletions modules/light-clients/08-wasm/testing/wasm_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ func (endpoint *WasmEndpoint) CreateClient() error {
checksum, err := types.CreateChecksum(Code)
require.NoError(endpoint.Chain.TB, err)

clientState := types.NewClientState(contractClientState, checksum, clienttypes.NewHeight(0, 1))
consensusState := types.NewConsensusState(contractConsensusState)
wrappedClientStateBz := clienttypes.MustMarshalClientState(endpoint.Chain.App.AppCodec(), CreateMockTendermintClientState(clienttypes.NewHeight(1, 5)))
wrappedClientConsensusStateBz := clienttypes.MustMarshalConsensusState(endpoint.Chain.App.AppCodec(), MockTendermintClientConsensusState)

clientState := types.NewClientState(wrappedClientStateBz, checksum, clienttypes.NewHeight(0, 1))
consensusState := types.NewConsensusState(wrappedClientConsensusStateBz)

msg, err := clienttypes.NewMsgCreateClient(
clientState, consensusState, endpoint.Chain.SenderAccount.GetAddress().String(),
Expand Down
5 changes: 3 additions & 2 deletions modules/light-clients/08-wasm/types/client_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,9 @@ func (cs ClientState) Initialize(ctx sdk.Context, cdc codec.BinaryCodec, clientS
}

payload := InstantiateMessage{
ClientState: &cs,
ConsensusState: consensusState,
ClientState: cs.Data,
ConsensusState: consensusState.Data,
Checksum: cs.Checksum,
}

return wasmInstantiate(ctx, cdc, clientStore, &cs, payload)
Expand Down
22 changes: 14 additions & 8 deletions modules/light-clients/08-wasm/types/client_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,15 @@ func (suite *TypesTestSuite) TestInitialize() {

suite.Require().Equal(env.Contract.Address, defaultWasmClientID)

store.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), payload.ClientState))
store.Set(host.ConsensusStateKey(payload.ClientState.LatestHeight), clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), payload.ConsensusState))
wrappedClientState := clienttypes.MustUnmarshalClientState(suite.chainA.App.AppCodec(), payload.ClientState)

clientState := types.NewClientState(payload.ClientState, payload.Checksum, wrappedClientState.GetLatestHeight().(clienttypes.Height))
clientStateBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), clientState)
store.Set(host.ClientStateKey(), clientStateBz)

consensusState := types.NewConsensusState(payload.ConsensusState)
consensusStateBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), consensusState)
store.Set(host.ConsensusStateKey(clientState.GetLatestHeight()), consensusStateBz)

resp, err := json.Marshal(types.EmptyResult{})
suite.Require().NoError(err)
Expand Down Expand Up @@ -305,18 +312,17 @@ func (suite *TypesTestSuite) TestInitialize() {
suite.Run(tc.name, func() {
suite.SetupWasmWithMockVM()

checksum, err := types.CreateChecksum(wasmtesting.Code)
suite.Require().NoError(err)

clientState = types.NewClientState([]byte{1}, checksum, clienttypes.NewHeight(0, 1))
consensusState = types.NewConsensusState([]byte{2})
wrappedClientStateBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), wasmtesting.MockTendermitClientState)
wrappedClientConsensusStateBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientConsensusState)
clientState = types.NewClientState(wrappedClientStateBz, suite.checksum, wasmtesting.MockTendermitClientState.GetLatestHeight().(clienttypes.Height))
consensusState = types.NewConsensusState(wrappedClientConsensusStateBz)

clientID := suite.chainA.App.GetIBCKeeper().ClientKeeper.GenerateClientIdentifier(suite.chainA.GetContext(), clientState.ClientType())
clientStore = suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), clientID)

tc.malleate()

err = clientState.Initialize(suite.chainA.GetContext(), suite.chainA.Codec, clientStore, consensusState)
err := clientState.Initialize(suite.chainA.GetContext(), suite.chainA.Codec, clientStore, consensusState)

expPass := tc.expError == nil
if expPass {
Expand Down
21 changes: 11 additions & 10 deletions modules/light-clients/08-wasm/types/contract_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import (

// InstantiateMessage is the message that is sent to the contract's instantiate entry point.
type InstantiateMessage struct {
ClientState *ClientState `json:"client_state"`
ConsensusState *ConsensusState `json:"consensus_state"`
ClientState []byte `json:"client_state"`
ConsensusState []byte `json:"consensus_state"`
Checksum []byte `json:"checksum"`
}

// QueryMsg is used to encode messages that are sent to the contract's query entry point.
Expand Down Expand Up @@ -36,12 +37,12 @@ type TimestampAtHeightMsg struct {

// VerifyClientMessageMsg is a queryMsg sent to the contract to verify a client message.
type VerifyClientMessageMsg struct {
ClientMessage *ClientMessage `json:"client_message"`
ClientMessage []byte `json:"client_message"`
}

// CheckForMisbehaviourMsg is a queryMsg sent to the contract to check for misbehaviour.
type CheckForMisbehaviourMsg struct {
ClientMessage *ClientMessage `json:"client_message"`
ClientMessage []byte `json:"client_message"`
}

// SudoMsg is used to encode messages that are sent to the contract's sudo entry point.
Expand All @@ -59,12 +60,12 @@ type SudoMsg struct {

// UpdateStateMsg is a sudoMsg sent to the contract to update the client state.
type UpdateStateMsg struct {
ClientMessage *ClientMessage `json:"client_message"`
ClientMessage []byte `json:"client_message"`
}

// UpdateStateOnMisbehaviourMsg is a sudoMsg sent to the contract to update its state on misbehaviour.
type UpdateStateOnMisbehaviourMsg struct {
ClientMessage *ClientMessage `json:"client_message"`
ClientMessage []byte `json:"client_message"`
}

// VerifyMembershipMsg is a sudoMsg sent to the contract to verify a membership proof.
Expand All @@ -88,10 +89,10 @@ type VerifyNonMembershipMsg struct {

// VerifyUpgradeAndUpdateStateMsg is a sudoMsg sent to the contract to verify an upgrade and update its state.
type VerifyUpgradeAndUpdateStateMsg struct {
UpgradeClientState ClientState `json:"upgrade_client_state"`
UpgradeConsensusState ConsensusState `json:"upgrade_consensus_state"`
ProofUpgradeClient []byte `json:"proof_upgrade_client"`
ProofUpgradeConsensusState []byte `json:"proof_upgrade_consensus_state"`
UpgradeClientState []byte `json:"upgrade_client_state"`
UpgradeConsensusState []byte `json:"upgrade_consensus_state"`
ProofUpgradeClient []byte `json:"proof_upgrade_client"`
ProofUpgradeConsensusState []byte `json:"proof_upgrade_consensus_state"`
}

// MigrateClientStore is a sudoMsg sent to the contract to verify a given substitute client and update to its state.
Expand Down
2 changes: 1 addition & 1 deletion modules/light-clients/08-wasm/types/misbehaviour_handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func (cs ClientState) CheckForMisbehaviour(ctx sdk.Context, _ codec.BinaryCodec,
}

payload := QueryMsg{
CheckForMisbehaviour: &CheckForMisbehaviourMsg{ClientMessage: clientMessage},
CheckForMisbehaviour: &CheckForMisbehaviourMsg{ClientMessage: clientMessage.Data},
}

result, err := wasmQuery[CheckForMisbehaviourResult](ctx, clientStore, &cs, payload)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import (

wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing"
"github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types"
clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
"github.com/cosmos/ibc-go/v8/modules/core/exported"
ibctmtypes "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint"
ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint"
)

func (suite *TypesTestSuite) TestCheckForMisbehaviour() {
Expand Down Expand Up @@ -61,7 +62,7 @@ func (suite *TypesTestSuite) TestCheckForMisbehaviour() {
},
{
"success: invalid client message", func() {
clientMessage = &ibctmtypes.Header{}
clientMessage = &ibctm.Header{}
// we will not register the callback here because this test case does not reach the VM
},
false,
Expand All @@ -78,7 +79,7 @@ func (suite *TypesTestSuite) TestCheckForMisbehaviour() {

clientState := endpoint.GetClientState()
clientMessage = &types.ClientMessage{
Data: []byte{1},
Data: clienttypes.MustMarshalClientMessage(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientMisbehaviour),
}

clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID)
Expand Down
14 changes: 0 additions & 14 deletions modules/light-clients/08-wasm/types/proposal_handle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import (
cosmwasm "github.com/CosmWasm/wasmvm"
wasmvmtypes "github.com/CosmWasm/wasmvm/types"

storetypes "cosmossdk.io/store/types"

sdk "github.com/cosmos/cosmos-sdk/types"

wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing"
"github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types"
clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
Expand Down Expand Up @@ -130,13 +126,3 @@ func (suite *TypesTestSuite) TestCheckSubstituteAndUpdateState() {
})
}
}

func GetProcessedHeight(clientStore storetypes.KVStore, height exported.Height) (uint64, bool) {
key := ibctm.ProcessedHeightKey(height)
bz := clientStore.Get(key)
if len(bz) == 0 {
return 0, false
}

return sdk.BigEndianToUint64(bz), true
}
11 changes: 9 additions & 2 deletions modules/light-clients/08-wasm/types/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,15 @@ func (suite *TypesTestSuite) setupWasmWithMockVM() (ibctesting.TestingApp, map[s
err := json.Unmarshal(initMsg, &payload)
suite.Require().NoError(err)

store.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), payload.ClientState))
store.Set(host.ConsensusStateKey(payload.ClientState.LatestHeight), clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), payload.ConsensusState))
wrappedClientState := clienttypes.MustUnmarshalClientState(suite.chainA.App.AppCodec(), payload.ClientState)

clientState := types.NewClientState(payload.ClientState, payload.Checksum, wrappedClientState.GetLatestHeight().(clienttypes.Height))
clientStateBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), clientState)
store.Set(host.ClientStateKey(), clientStateBz)

consensusState := types.NewConsensusState(payload.ConsensusState)
consensusStateBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), consensusState)
store.Set(host.ConsensusStateKey(clientState.GetLatestHeight()), consensusStateBz)

resp, err := json.Marshal(types.EmptyResult{})
suite.Require().NoError(err)
Expand Down
6 changes: 3 additions & 3 deletions modules/light-clients/08-wasm/types/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (cs ClientState) VerifyClientMessage(ctx sdk.Context, _ codec.BinaryCodec,
}

payload := QueryMsg{
VerifyClientMessage: &VerifyClientMessageMsg{ClientMessage: clientMessage},
VerifyClientMessage: &VerifyClientMessageMsg{ClientMessage: clientMessage.Data},
}
_, err := wasmQuery[EmptyResult](ctx, clientStore, &cs, payload)
return err
Expand All @@ -40,7 +40,7 @@ func (cs ClientState) UpdateState(ctx sdk.Context, cdc codec.BinaryCodec, client
}

payload := SudoMsg{
UpdateState: &UpdateStateMsg{ClientMessage: clientMessage},
UpdateState: &UpdateStateMsg{ClientMessage: clientMessage.Data},
}

result, err := wasmSudo[UpdateStateResult](ctx, cdc, clientStore, &cs, payload)
Expand All @@ -65,7 +65,7 @@ func (cs ClientState) UpdateStateOnMisbehaviour(ctx sdk.Context, cdc codec.Binar
}

payload := SudoMsg{
UpdateStateOnMisbehaviour: &UpdateStateOnMisbehaviourMsg{ClientMessage: clientMessage},
UpdateStateOnMisbehaviour: &UpdateStateOnMisbehaviourMsg{ClientMessage: clientMessage.Data},
}

_, err := wasmSudo[EmptyResult](ctx, cdc, clientStore, &cs, payload)
Expand Down
Loading

0 comments on commit afd5b52

Please sign in to comment.