Contributing to Datasets
Contribution Model
We implemented a two-phase contribution model that enables controlled additions to datasets while maintaining data integrity. Each contribution is represented as an NFT, providing clear ownership tracking and transferability of datasets.
Contribution Flow
The contribution process consists of two main steps:
- Initiation: A potential contributor creates a pending contribution
- Completion: The dataset owner approves the contribution, minting an NFT, if denies, does nothing.
Initiating Contributions
CLI Command
nuklaid tx dataset initiate-contribute-dataset <denom> <data-location> <data-identifier> --from=<mykey>
Example
nuklaid tx dataset initiate-contribute-dataset nuklaib294bd8 "ipfs://QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX" "temperature-data-2021" --from=bob
Implementation
func (k msgServer) InitiateContributeDataset(goCtx context.Context, msg *types.MsgInitiateContributeDataset) (*types.MsgInitiateContributeDatasetResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
// Check if the dataset exists
dataset, found := k.GetDataset(ctx, msg.Denom)
if !found {
return nil, errorsmod.Wrap(sdkerrors.ErrNotFound, "dataset does not exist")
}
// Check if the caller is the owner or if it's a community dataset
if msg.Owner != dataset.Owner {
if !dataset.IsCommunityDataset {
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "only the owner can contribute to this dataset")
}
}
// Generate a unique contribution ID
contributionId := types.ComputeContributionID(msg.Denom, msg.DataLocation, msg.DataIdentifier)
// Check if the contribution already exists
_, found = k.GetPendingDatasetContribution(ctx, contributionId)
if found {
return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "contribution already exists")
}
// Create a new pending contribution
pendingContribution := types.PendingDatasetContribution{
ContributionId: contributionId,
Denom: msg.Denom,
DataContributor: msg.Owner,
DataLocation: msg.DataLocation,
DataIdentifier: msg.DataIdentifier,
}
// Store the pending contribution
k.SetPendingDatasetContribution(ctx, pendingContribution)
return &types.MsgInitiateContributeDatasetResponse{
ContributionId: contributionId,
}, nil
}
Completing Contributions
CLI Command
nuklaid tx dataset complete-contribute-dataset <denom> <contribution-id> --from=<key-name>
Example
nuklaid tx dataset complete-contribute-dataset nuklaidataset07d1b507e614c562ef726c4b051 049be817a01a4bb71924bea26f041233 --from=bob
Implementation
func (k msgServer) CompleteContributeDataset(goCtx context.Context, msg *types.MsgCompleteContributeDataset) (*types.MsgCompleteContributeDatasetResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
// Retrieve the pending contribution
pendingContribution, found := k.GetPendingDatasetContribution(ctx, msg.ContributionId)
if !found {
return nil, errorsmod.Wrap(sdkerrors.ErrKeyNotFound, "pending contribution not found")
}
// Retrieve the dataset
dataset, found := k.GetDataset(ctx, pendingContribution.Denom)
if !found {
return nil, errorsmod.Wrap(sdkerrors.ErrKeyNotFound, "dataset not found")
}
// Ensure only the dataset owner can complete the contribution
if msg.Owner != dataset.Owner {
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "only dataset owner can complete contribution")
}
// Mint the NFT
nftId := fmt.Sprintf("%s.%s", msg.Denom, msg.ContributionId)
nftData := map[string]string{
"dataContributor": pendingContribution.DataContributor,
"dataLocation": pendingContribution.DataLocation,
"dataIdentifier": pendingContribution.DataIdentifier,
}
nftDataBytes, err := json.Marshal(nftData)
if err != nil {
return nil, errorsmod.Wrap(err, "failed to encode NFT data")
}
token := nft.NFT{
ClassId: dataset.Denom,
Id: nftId,
Uri: "",
UriHash: "",
Data: &sdkCodec.Any{Value: []byte(nftDataBytes)},
}
contributorAddr, err := sdk.AccAddressFromBech32(pendingContribution.DataContributor)
if err != nil {
return nil, errorsmod.Wrap(err, "invalid contributor address")
}
if err := k.nftKeeper.Mint(ctx, token, contributorAddr); err != nil {
return nil, errorsmod.Wrap(err, "failed to mint NFT")
}
// Remove the pending contribution
k.RemovePendingDatasetContribution(ctx, msg.ContributionId)
// Update the dataset supply
updatedDataset := types.Dataset{
Owner: dataset.Owner,
Denom: dataset.Denom,
// Other fields preserved...
Supply: dataset.Supply + 1,
// Other fields preserved...
}
k.SetDataset(ctx, updatedDataset)
return &types.MsgCompleteContributeDatasetResponse{
NftId: nftId,
}, nil
}
Contribution Ownership
Each approved contribution is represented as an NFT owned by the contributor. This NFT can be transferred to other addresses.
Transferring Contribution Ownership
nuklaid tx dataset update-contribution-owner <denom> <contribution-id> <new-owner> --from=<key-name>
func (k msgServer) UpdateContributionOwner(goCtx context.Context, msg *types.MsgUpdateContributionOwner) (*types.MsgUpdateContributionOwnerResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
// Retrieve the NFT contribution
contribution, found := k.nftKeeper.GetNFT(ctx, msg.Denom, msg.ContributionId)
if !found {
return nil, errorsmod.Wrap(sdkerrors.ErrNotFound, "contribution not found")
}
// Extract NFT metadata
var metadata map[string]string
if contribution.Data != nil && len(contribution.Data.Value) > 0 {
if err := json.Unmarshal(contribution.Data.Value, &metadata); err != nil {
return nil, errorsmod.Wrap(err, "failed to decode NFT metadata")
}
}
// Validate current owner
currentOwner, exists := metadata["dataContributor"]
if !exists || currentOwner != msg.Owner {
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "only the current data contributor can transfer ownership")
}
// Convert new owner address
newOwnerAddr, err := sdk.AccAddressFromBech32(msg.NewOwner)
if err != nil {
return nil, errorsmod.Wrap(err, "invalid new owner address")
}
// Transfer the NFT ownership
if err := k.nftKeeper.Transfer(ctx, msg.Denom, msg.ContributionId, newOwnerAddr); err != nil {
return nil, errorsmod.Wrap(err, "failed to transfer NFT")
}
// Update NFT metadata
metadata["dataContributor"] = msg.NewOwner
updatedMetadataBytes, err := json.Marshal(metadata)
if err != nil {
return nil, errorsmod.Wrap(err, "failed to encode updated NFT metadata")
}
// Update NFT with new metadata
updatedNFT := nft.NFT{
ClassId: contribution.ClassId,
Id: contribution.Id,
Uri: contribution.Uri,
UriHash: contribution.UriHash,
Data: &sdkCodec.Any{Value: updatedMetadataBytes},
}
// Save updated NFT
if err := k.nftKeeper.Update(ctx, updatedNFT); err != nil {
return nil, errorsmod.Wrap(err, "failed to update NFT metadata")
}
return &types.MsgUpdateContributionOwnerResponse{}, nil
}
Querying Contributions
List All Contributions
nuklaid query dataset list-contribution <denom>
Get Specific Contribution
nuklaid query dataset show-contribution <denom> <contribution-id>
List Contributions by Owner
nuklaid query dataset list-contribution-by-owner <denom> <owner>
Find Contribution Owner
nuklaid query dataset show-contribution-owner <denom> <contribution-id>
Contribution Storage
Pending contributions are stored in a dedicated KVStore:
func (k Keeper) SetPendingDatasetContribution(ctx context.Context, pendingDatasetContribution types.PendingDatasetContribution) {
storeAdapter := runtime.KVStoreAdapter(k.storeService.OpenKVStore(ctx))
store := prefix.NewStore(storeAdapter, types.KeyPrefix(types.PendingDatasetContributionKeyPrefix))
b := k.cdc.MustMarshal(&pendingDatasetContribution)
store.Set(types.PendingDatasetContributionKey(pendingDatasetContribution.ContributionId), b)
}