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

JoystreamJS: Generate commitment from arbitrary bytes source #4584

Merged
merged 2 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions cli/src/commands/content/channelPayoutProofAtByteOffset.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { channelPayoutProofAtByteOffset } from '@joystream/js/content'
import { readBytesFromFile } from '@joystream/js/utils'
import { Command, flags } from '@oclif/command'
import { displayCollapsedRow } from '../../helpers/display'

Expand Down Expand Up @@ -36,8 +37,8 @@ export default class ChannelPayoutProofAtByteOffset extends Command {
}

const payoutProof = path
? await channelPayoutProofAtByteOffset('PATH', path, start)
: await channelPayoutProofAtByteOffset('URL', url!, start)
? await channelPayoutProofAtByteOffset(readBytesFromFile('PATH', path), start)
: await channelPayoutProofAtByteOffset(readBytesFromFile('URL', url!), start)

displayCollapsedRow({
'Channel Id': payoutProof.channelId,
Expand Down
5 changes: 3 additions & 2 deletions cli/src/commands/content/channelPayoutsPayloadHeader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChannelPayoutsMetadata } from '@joystream/metadata-protobuf'
import { serializedPayloadHeader } from '@joystream/js/content'
import { readBytesFromFile } from '@joystream/js/utils'
import { Command, flags } from '@oclif/command'
import chalk from 'chalk'
import { displayCollapsedRow, displayTable } from '../../helpers/display'
Expand Down Expand Up @@ -27,8 +28,8 @@ export default class ChannelPayoutPayloadHeader extends Command {

try {
const serializedHeader = path
? await serializedPayloadHeader('PATH', path)
: await serializedPayloadHeader('URL', url!)
? await serializedPayloadHeader(readBytesFromFile('PATH', path))
: await serializedPayloadHeader(readBytesFromFile('URL', url!))

const header = ChannelPayoutsMetadata.Header.decode(serializedHeader)
this.log(
Expand Down
5 changes: 3 additions & 2 deletions cli/src/commands/content/generateChannelPayoutsCommitment.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { generateCommitmentFromPayloadFile } from '@joystream/js/content'
import { readBytesFromFile } from '@joystream/js/utils'
import { flags } from '@oclif/command'
import chalk from 'chalk'
import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
Expand Down Expand Up @@ -27,8 +28,8 @@ export default class GenerateChannelPayoutsCommitment extends ContentDirectoryCo

try {
const commitment = path
? await generateCommitmentFromPayloadFile('PATH', path)
: await generateCommitmentFromPayloadFile('URL', url!)
? await generateCommitmentFromPayloadFile(readBytesFromFile('PATH', path))
: await generateCommitmentFromPayloadFile(readBytesFromFile('URL', url!))

this.log(chalk.green(`Channel Payout payload merkle root is ${chalk.cyanBright(commitment)}!`))
} catch (error) {
Expand Down
54 changes: 22 additions & 32 deletions joystreamjs/src/content/channelPayouts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { MerkleTree } from 'merkletreejs'
import { Reader, Writer } from 'protobufjs'
import { ChannelPayoutProof } from '../../typings/ChannelPayoutsPayload.schema'
import { ChannelPayoutsVector } from '../../typings/ChannelPayoutsVector.schema'
import { asValidatedMetadata, readBytesFromFile, ReadFileContext } from '../utils'
import { asValidatedMetadata, ReadBytes, readBytesFromFile, ReadFileContext } from '../utils'
import { Buffer } from 'buffer'

export const hashFunc = blake2AsU8a
Expand All @@ -35,15 +35,18 @@ export async function channelPayoutProof(
pathOrUrl: string,
channelId: number
): Promise<ChannelPayoutsMetadata.Body.ChannelPayoutProof> {
const serializedHeader = await serializedPayloadHeader(context, pathOrUrl)
const serializedHeader = await serializedPayloadHeader(readBytesFromFile(context, pathOrUrl))
const header = ChannelPayoutsMetadata.Header.decode(serializedHeader)

const channelPayoutProofOffset = header.channelPayoutByteOffsets.find((o) => o.channelId === channelId)
if (!channelPayoutProofOffset) {
throw new Error(`No payout Proof exists for channel id ${channelId}`)
}

return channelPayoutProofAtByteOffset(context, pathOrUrl, Number(channelPayoutProofOffset.byteOffset))
return channelPayoutProofAtByteOffset(
readBytesFromFile(context, pathOrUrl),
Number(channelPayoutProofOffset.byteOffset)
)
}

/**
Expand Down Expand Up @@ -104,19 +107,14 @@ function lengthOfVarintEncodedMessageSize(protobufMessageLength: number): number
* so we arbitrary read `n` bytes from the payload based on the assumption that the size of the header CAN BE
* encoded in `n` bytes. For reference, if serialized message is over 4 TB then its size information can be
* encoded in just 6 bytes
* @param context "PATH" | "URL"
* @param pathOrUrl path to protobuf serialized payload file
* @param read getter which returns the requested sequence of bytes
* @param messageOffset byte offset of message in serialized payload
* @returns length of serialized message in number of bytes
*/
async function lengthOfProtobufMessage(
context: ReadFileContext,
pathOrUrl: string,
messageOffset: number
): Promise<number> {
async function lengthOfProtobufMessage(read: ReadBytes, messageOffset: number): Promise<number> {
// TODO: improve the implementation by reading size info byte by byte
// TODO: and checking most significant bit (msb) of each byte.
const arbitraryBytes = await readBytesFromFile(context, pathOrUrl, messageOffset, messageOffset + 10)
const arbitraryBytes = await read(messageOffset, messageOffset + 10)
const lengthOfMessage = Reader.create(arbitraryBytes).uint32()
return lengthOfMessage
}
Expand All @@ -143,17 +141,14 @@ function lengthOfHeader(numberOfChannels: number): number {

/**
* Get serialized payload header from a local file.
* @param context "PATH" | "URL"
* @param pathOrUrl path to protobuf serialized payload file
* @param read getter which returns the requested sequence of bytes
* @return bytes of payload header
**/
export async function serializedPayloadHeader(context: ReadFileContext, pathOrUrl: string): Promise<Uint8Array> {
export async function serializedPayloadHeader(read: ReadBytes): Promise<Uint8Array> {
// skip the first byte which is the Tag(key) of `Header` message
const lengthOfSerializedHeader = await lengthOfProtobufMessage(context, pathOrUrl, 1)
const lengthOfSerializedHeader = await lengthOfProtobufMessage(read, 1)
const lengthOfVarintEncodedHeaderSize = lengthOfVarintEncodedMessageSize(lengthOfSerializedHeader)
const serializedHeader = await readBytesFromFile(
context,
pathOrUrl,
const serializedHeader = await read(
1 + lengthOfVarintEncodedHeaderSize,
lengthOfVarintEncodedHeaderSize + lengthOfSerializedHeader
)
Expand All @@ -163,21 +158,17 @@ export async function serializedPayloadHeader(context: ReadFileContext, pathOrUr

/**
* Get channel payout Proof from local serialized payload file.
* @param context "PATH" | "URL"
* @param pathOrUrl path to protobuf serialized payload file
* @param read getter which returns the requested sequence of bytes
* @param byteOffset byte offset of channel payout Proof in serialized payload
* @return channel payout Proof
**/
export async function channelPayoutProofAtByteOffset(
context: ReadFileContext,
pathOrUrl: string,
read: ReadBytes,
byteOffset: number
): Promise<ChannelPayoutsMetadata.Body.ChannelPayoutProof> {
const lengthOfSerializedProof = await lengthOfProtobufMessage(context, pathOrUrl, byteOffset)
const lengthOfSerializedProof = await lengthOfProtobufMessage(read, byteOffset)
const lengthOfVarintEncodedProofSize = lengthOfVarintEncodedMessageSize(lengthOfSerializedProof)
const serializedPayoutProof = await readBytesFromFile(
context,
pathOrUrl,
const serializedPayoutProof = await read(
byteOffset + lengthOfVarintEncodedProofSize,
byteOffset + lengthOfSerializedProof + 1
)
Expand All @@ -187,18 +178,17 @@ export async function channelPayoutProofAtByteOffset(

/**
* Generate merkle root from the serialized payload
* @param context "PATH" | "URL"
* @param pathOrUrl path to protobuf serialized payload file
* @param read getter which returns the requested sequence of bytes
* @returns merkle root of the cashout vector
*/
export async function generateCommitmentFromPayloadFile(context: ReadFileContext, pathOrUrl: string): Promise<string> {
const serializedHeader = await serializedPayloadHeader(context, pathOrUrl)
export async function generateCommitmentFromPayloadFile(read: ReadBytes): Promise<string> {
const serializedHeader = await serializedPayloadHeader(read)
const header = ChannelPayoutsMetadata.Header.decode(serializedHeader)

// Any payout Proof can be used to generate the merkle root,
// here first Proof from channel payouts payload is used
const ProofByteOffset = header.channelPayoutByteOffsets.shift()!.byteOffset.toNumber()
const proof = await channelPayoutProofAtByteOffset(context, pathOrUrl, ProofByteOffset)
const ProofByteOffset = Number(header.channelPayoutByteOffsets.shift()?.byteOffset)
const proof = await channelPayoutProofAtByteOffset(read, ProofByteOffset)
return verifyChannelPayoutProof(proof)
}

Expand Down
53 changes: 28 additions & 25 deletions joystreamjs/src/utils/InputOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,43 @@ import fs from 'fs'

export type ReadFileContext = 'PATH' | 'URL'

/**
* Read sequence of bytes from arbitrary source. Both `start` and `end` are inclusive
* @param start starting indexj
* @param end ending index of the range
* @returns byte sequence
*/
export type ReadBytes = (start: number, end: number) => Promise<Uint8Array>

/**
* Read sequence of bytes from the file or remote host
* provided path. Both `start` and `end` are inclusive
* @param context path to the file
* @param pathOrUrl
* @param start starting index of the range
* @param end ending index of the range
* @returns byte sequence
* @returns ReadBytes
*/
export async function readBytesFromFile(
context: ReadFileContext,
pathOrUrl: string,
start: number,
end: number
): Promise<Uint8Array> {
try {
if (context === 'PATH') {
return new Promise((resolve) => {
const a = fs.createReadStream(pathOrUrl, { start, end }).on('data', (data) => {
resolve(data as Buffer)
a.close()
export function readBytesFromFile(context: ReadFileContext, pathOrUrl: string): ReadBytes {
return async (start, end) => {
try {
if (context === 'PATH') {
return new Promise((resolve) => {
const a = fs.createReadStream(pathOrUrl, { start, end }).on('data', (data) => {
resolve(data as Buffer)
a.close()
})
})
}

const response = await axios.get<Buffer>(pathOrUrl, {
responseType: 'arraybuffer',
headers: {
range: `bytes=${start}-${end}`,
},
})
return new Uint8Array(response.data)
} catch (error) {
throw new Error(`Failed to read input stream`)
}

const response = await axios.get<Buffer>(pathOrUrl, {
responseType: 'arraybuffer',
headers: {
range: `bytes=${start}-${end}`,
},
})
return new Uint8Array(response.data)
} catch (error) {
throw new Error(`Failed to read input stream`)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createType } from '@joystream/types'
import { MemberId } from '@joystream/types/primitives'
import { generateCommitmentFromPayloadFile } from '@joystream/js/content'
import { readBytesFromFile } from '@joystream/js/utils'
import BN from 'bn.js'
import fs from 'fs'
import { Api } from '../../../Api'
Expand Down Expand Up @@ -43,7 +44,7 @@ export class UpdateChannelPayoutsProposalFixture extends BaseQueryNodeFixture {
{
type: 'UpdateChannelPayouts',
details: createType('PalletContentUpdateChannelPayoutsParametersRecord', {
commitment: await generateCommitmentFromPayloadFile('PATH', protobufPayloadFilePath),
commitment: await generateCommitmentFromPayloadFile(readBytesFromFile('PATH', protobufPayloadFilePath)),
payload: {
objectCreationParams: {
size_: fs.statSync(protobufPayloadFilePath).size,
Expand Down