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

Feature/app action #4631

Merged
merged 22 commits into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
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
26 changes: 26 additions & 0 deletions metadata-protobuf/proto/App.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
syntax = "proto2";

import "proto/Video.proto";
import "proto/Channel.proto";
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved

message AppActionMetadata {
// YouTube video ID
optional string video_id = 2;
}

message AppAction {
// ID of application
required string app_id = 999;

// Metadata
optional bytes metadata = 2;

// Raw metadata of wrapped action
optional bytes raw_action = 3;

// Signature over app commitment
optional string signature = 4;
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved

// Nonce to prevent signature reusal
optional string nonce = 5;
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 1 addition & 1 deletion query-node/mappings/src/content/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export async function processDeleteAppMessage(
logger.info('App has been removed', { appId })
}

async function getAppById(store: DatabaseManager, appId: string): Promise<App | undefined> {
export async function getAppById(store: DatabaseManager, appId: string): Promise<App | undefined> {
const app = await store.get(App, {
where: {
id: appId,
Expand Down
60 changes: 50 additions & 10 deletions query-node/mappings/src/content/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
eslint-disable @typescript-eslint/naming-convention
*/
import { DatabaseManager, EventContext, StoreContext, SubstrateEvent } from '@joystream/hydra-common'
import { ChannelMetadata, ChannelModeratorRemarked, ChannelOwnerRemarked } from '@joystream/metadata-protobuf'
import {
AppAction,
ChannelMetadata,
ChannelModeratorRemarked,
ChannelOwnerRemarked,
} from '@joystream/metadata-protobuf'
import { ChannelId, DataObjectId } from '@joystream/types/primitives'
import {
Channel,
Expand Down Expand Up @@ -42,6 +47,9 @@ import {
processChannelMetadata,
unsetAssetRelations,
mapAgentPermission,
processAppActionMetadata,
generateAppActionCommitment,
u8aToBytes,
} from './utils'
import { BTreeMap, BTreeSet, u64 } from '@polkadot/types'
// Joystream types
Expand All @@ -53,6 +61,9 @@ export async function content_ChannelCreated(ctx: EventContext & StoreContext):
const [channelId, { owner, dataObjects, channelStateBloatBond }, channelCreationParameters, rewardAccount] =
new Content.ChannelCreatedEvent(event).params

// prepare channel owner (handles fields `ownerMember` and `ownerCuratorGroup`)
const channelOwner = await convertChannelOwnerToMemberOrCuratorGroup(store, owner)

// create entity
const channel = new Channel({
// main data
Expand All @@ -61,10 +72,7 @@ export async function content_ChannelCreated(ctx: EventContext & StoreContext):
videos: [],
createdInBlock: event.blockNumber,
activeVideosCounter: 0,

// prepare channel owner (handles fields `ownerMember` and `ownerCuratorGroup`)
...(await convertChannelOwnerToMemberOrCuratorGroup(store, owner)),

...channelOwner,
rewardAccount: rewardAccount.toString(),
channelStateBloatBond: channelStateBloatBond.amount,
})
Expand All @@ -77,13 +85,37 @@ export async function content_ChannelCreated(ctx: EventContext & StoreContext):
inconsistentState(`storageBag for channel ${channelId} does not exist`)
}

const metadata = deserializeMetadata(ChannelMetadata, channelCreationParameters.meta.unwrap()) || {}
await processChannelMetadata(ctx, channel, metadata, dataObjects)
const appAction = deserializeMetadata(AppAction, channelCreationParameters.meta.unwrap())
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved

if (appAction && Object.keys(appAction).length) {
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved
const channelMetadataBytes = u8aToBytes(appAction.rawAction)
const channelMetadata = deserializeMetadata(ChannelMetadata, channelMetadataBytes)
const appCommitment = generateAppActionCommitment(
channelOwner.ownerMember?.totalChannelsCreated ?? -1,
channelOwner.ownerMember?.id ? `m:${channelOwner.ownerMember.id}` : `c:${channelOwner.ownerCuratorGroup?.id}`,
channelCreationParameters.assets.toU8a(),
appAction.rawAction ? channelMetadataBytes : undefined,
appAction.metadata ? u8aToBytes(appAction.metadata) : undefined
)
await processAppActionMetadata(
ctx,
channel,
appAction,
{ ownerNonce: channelOwner.ownerMember?.totalChannelsCreated, appCommitment },
(entity: Channel) => processChannelMetadata(ctx, entity, channelMetadata ?? {}, dataObjects)
)
} else {
const channelMetadata = deserializeMetadata(ChannelMetadata, channelCreationParameters.meta.unwrap()) ?? {}
await processChannelMetadata(ctx, channel, channelMetadata, dataObjects)
}
}

// save entity
await store.save<Channel>(channel)

if (channelOwner.ownerMember) {
channelOwner.ownerMember.totalChannelsCreated += 1
await store.save<Membership>(channelOwner.ownerMember)
}
// update channel permissions
await updateChannelAgentsPermissions(store, channel, channelCreationParameters.collaborators)

Expand Down Expand Up @@ -117,8 +149,16 @@ export async function content_ChannelUpdated(ctx: EventContext & StoreContext):
inconsistentState(`storageBag for channel ${channelId} does not exist`)
}

const newMetadata = deserializeMetadata(ChannelMetadata, newMetadataBytes) || {}
await processChannelMetadata(ctx, channel, newMetadata, newDataObjects)
const newMetadata = deserializeMetadata(AppAction, newMetadataBytes)
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved

if (newMetadata && 'rawAction' in newMetadata) {
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved
const channelMetadataBytes = u8aToBytes(newMetadata.rawAction)
const channelMetadata = deserializeMetadata(ChannelMetadata, channelMetadataBytes)
await processChannelMetadata(ctx, channel, channelMetadata ?? {}, newDataObjects)
} else {
const realNewMetadata = deserializeMetadata(ChannelMetadata, newMetadataBytes)
await processChannelMetadata(ctx, channel, realNewMetadata ?? {}, newDataObjects)
}
}

// save channel
Expand Down
88 changes: 87 additions & 1 deletion query-node/mappings/src/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import {
IMediaType,
IChannelMetadata,
ISubtitleMetadata,
IAppAction,
} from '@joystream/metadata-protobuf'
import { integrateMeta, isSet, isValidLanguageCode } from '@joystream/metadata-protobuf/utils'
import { ed25519Verify } from '@polkadot/util-crypto'
import { invalidMetadata, inconsistentState, logger, deterministicEntityId, EntityType } from '../common'
import {
// primary entities
Expand All @@ -19,6 +21,7 @@ import {
Language,
License,
VideoMediaMetadata,
App,
// asset
Membership,
VideoMediaEncoding,
Expand All @@ -41,12 +44,16 @@ import {
PalletContentPermissionsContentActor as ContentActor,
PalletContentIterableEnumsChannelActionPermission,
} from '@polkadot/types/lookup'
import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types'
import { AnyMetadataClass, DecodedMetadataObject } from '@joystream/metadata-protobuf/types'
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved
import BN from 'bn.js'
import _ from 'lodash'
import { getSortedDataObjectsByIds } from '../storage/utils'
import { BTreeSet } from '@polkadot/types'
import { DataObjectId } from '@joystream/types/primitives'
import { Bytes } from '@polkadot/types/primitive'
import { u8aToHex, stringToHex } from '@polkadot/util'
import { createType } from '@joystream/types'
import { getAppById } from './app'

const ASSET_TYPES = {
channel: [
Expand Down Expand Up @@ -166,6 +173,64 @@ async function processVideoSubtitleAssets(
}
}

async function validateAndGetApp<T>(
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved
ctx: EventContext & StoreContext,
validationContext: {
ownerNonce: number | undefined
appCommitment: string | undefined
},
appAction: DecodedMetadataObject<IAppAction>
): Promise<App | undefined> {
// If one is missing we cannot verify the signature
if (!appAction.appId || !appAction.signature || !appAction.nonce || !validationContext.appCommitment) {
invalidMetadata('Missing action fields to verify app')
return undefined
}

const app = await getAppById(ctx.store, appAction.appId)

if (!app || !app.authKey) {
invalidMetadata('No app of given id found')
return undefined
}

if (
typeof validationContext.ownerNonce === 'undefined' ||
validationContext.ownerNonce !== parseInt(appAction.nonce)
) {
invalidMetadata('Invalid app action nonce')

return undefined
}

try {
return ed25519Verify(validationContext.appCommitment, appAction.signature, app.authKey) ? app : undefined
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
invalidMetadata((e as Error)?.message)
return undefined
}
}

export async function processAppActionMetadata<T extends { entryApp?: App }>(
ctx: EventContext & StoreContext,
entity: T,
meta: DecodedMetadataObject<IAppAction>,
validationContext: {
ownerNonce: number | undefined
appCommitment: string | undefined
},
entityMetadataProcessor: (entity: T) => Promise<T>
): Promise<T> {
const app = await validateAndGetApp(ctx, validationContext, meta)
if (!app) {
return entityMetadataProcessor(entity)
}

integrateMeta(entity, { entryApp: app }, ['entryApp'])

return entityMetadataProcessor(entity)
}

export async function processChannelMetadata(
ctx: EventContext & StoreContext,
channel: Channel,
Expand Down Expand Up @@ -688,3 +753,24 @@ export async function unsetAssetRelations(store: DatabaseManager, dataObject: St
export function mapAgentPermission(permission: PalletContentIterableEnumsChannelActionPermission): string {
return permission.toString()
}

export function generateAppActionCommitment(
nonce: number,
creatorId: string,
assets: Uint8Array,
rawAction?: Bytes,
rawAppActionMetadata?: Bytes
): string {
const rawCommitment = [
nonce,
creatorId,
u8aToHex(assets),
...(rawAction ? u8aToHex(rawAction) : []),
...(rawAppActionMetadata ? u8aToHex(rawAppActionMetadata) : []),
]
return stringToHex(JSON.stringify(rawCommitment))
}

export function u8aToBytes(array?: DecodedMetadataObject<Uint8Array> | null): Bytes {
return createType('Bytes', array ? u8aToHex(array as Uint8Array) : '')
}
63 changes: 55 additions & 8 deletions query-node/mappings/src/content/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
eslint-disable @typescript-eslint/naming-convention
*/
import { DatabaseManager, EventContext, StoreContext } from '@joystream/hydra-common'
import { ContentMetadata, IVideoMetadata } from '@joystream/metadata-protobuf'
import { AppAction, AppActionMetadata, ContentMetadata, IAppAction, IVideoMetadata } from '@joystream/metadata-protobuf'
import { ChannelId, DataObjectId, VideoId } from '@joystream/types/primitives'
import {
PalletContentPermissionsContentActor as ContentActor,
Expand Down Expand Up @@ -34,6 +34,7 @@ import {
VideoDeletedEvent,
VideoVisibilitySetByModeratorEvent,
VideoSubtitle,
Membership,
} from 'query-node/dist/model'
import { Content } from '../../generated/types'
import { bytesToString, deserializeMetadata, genericEventFields, inconsistentState, logger } from '../common'
Expand All @@ -43,11 +44,15 @@ import { createNft } from './nft'
import {
convertContentActor,
convertContentActorToChannelOrNftOwner,
generateAppActionCommitment,
processAppActionMetadata,
processVideoMetadata,
u8aToBytes,
unsetAssetRelations,
videoRelationsForCounters,
} from './utils'
import { BTreeSet } from '@polkadot/types'
import { integrateMeta } from '@joystream/metadata-protobuf/utils'

interface ContentCreatedEventData {
contentActor: ContentActor
Expand Down Expand Up @@ -93,19 +98,25 @@ export async function content_ContentCreated(ctx: EventContext & StoreContext):
}

// deserialize & process metadata
const appAction = meta.isSome ? deserializeMetadata(AppAction, meta.unwrap()) : undefined
const contentMetadata = meta.isSome ? deserializeMetadata(ContentMetadata, meta.unwrap()) : undefined
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved

// Content Creation Preference
// 1. metadata == `VideoMetadata` || undefined -> create Video
// 1. metadata == `PlaylistMetadata` -> create Playlist (Not Supported Yet)

await processCreateVideoMessage(ctx, channel, contentMetadata?.videoMetadata || undefined, contentCreatedEventData)
await processCreateVideoMessage(
ctx,
channel,
appAction?.rawAction ? appAction : contentMetadata?.videoMetadata ?? undefined,
contentCreatedEventData
)
}

export async function processCreateVideoMessage(
ctx: EventContext & StoreContext,
channel: Channel,
metadata: DecodedMetadataObject<IVideoMetadata> | undefined,
metadata: DecodedMetadataObject<IAppAction> | DecodedMetadataObject<IVideoMetadata> | undefined,
contentCreatedEventData: ContentCreatedEventData
): Promise<void> {
const { store, event } = ctx
Expand All @@ -123,12 +134,42 @@ export async function processCreateVideoMessage(
reactionsCount: 0,
})

if (metadata) {
await processVideoMetadata(ctx, video, metadata, newDataObjectIds)
if (metadata && 'rawAction' in metadata) {
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved
const contentMetadataBytes = u8aToBytes(metadata.rawAction)
const videoMetadata = deserializeMetadata(ContentMetadata, contentMetadataBytes)?.videoMetadata ?? {}
const appActionMetadataBytes = metadata.metadata ? u8aToBytes(metadata.metadata) : undefined

const appCommitment = generateAppActionCommitment(
channel.ownerMember?.totalVideosCreated ?? -1,
channel.id ?? '',
contentCreationParameters.assets.toU8a(),
metadata.rawAction ? contentMetadataBytes : undefined,
appActionMetadataBytes
)
await processAppActionMetadata(
ctx,
video,
metadata,
{ ownerNonce: channel.ownerMember?.totalVideosCreated, appCommitment },
(entity) => {
if ('entryApp' in entity && appActionMetadataBytes) {
const appActionMetadata = deserializeMetadata(AppActionMetadata, appActionMetadataBytes)

appActionMetadata?.videoId && integrateMeta(entity, { ytVideoId: appActionMetadata.videoId }, ['ytVideoId'])
}
return processVideoMetadata(ctx, entity, videoMetadata, newDataObjectIds)
}
)
} else if (metadata) {
await processVideoMetadata(ctx, video, metadata as DecodedMetadataObject<IVideoMetadata>, newDataObjectIds)
}

// save video
await store.save<Video>(video)
if (channel.ownerMember) {
channel.ownerMember.totalVideosCreated += 1
await store.save<Membership>(channel.ownerMember)
}

if (contentCreationParameters.autoIssueNft.isSome) {
const issuanceParameters = contentCreationParameters.autoIssueNft.unwrap()
Expand Down Expand Up @@ -184,9 +225,15 @@ export async function content_ContentUpdated(ctx: EventContext & StoreContext):
})

if (video) {
const contentMetadata = newMeta.isSome ? deserializeMetadata(ContentMetadata, newMeta.unwrap()) : undefined

await processUpdateVideoMessage(ctx, video, contentMetadata?.videoMetadata || undefined, contentUpdatedEventData)
const appAction = newMeta.isSome ? deserializeMetadata(AppAction, newMeta.unwrap()) : undefined
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved
if (appAction && 'signature' in appAction) {
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved
const contentMetadataBytes = u8aToBytes(appAction?.rawAction)
WRadoslaw marked this conversation as resolved.
Show resolved Hide resolved
const videoMetadata = deserializeMetadata(ContentMetadata, contentMetadataBytes)?.videoMetadata
await processUpdateVideoMessage(ctx, video, videoMetadata ?? undefined, contentUpdatedEventData)
} else {
const contentMetadata = newMeta.isSome ? deserializeMetadata(ContentMetadata, newMeta.unwrap()) : undefined
await processUpdateVideoMessage(ctx, video, contentMetadata?.videoMetadata || undefined, contentUpdatedEventData)
}
return
}

Expand Down
Loading