Skip to content

Commit

Permalink
Merge pull request #450 from input-output-hk/feat/ogmios-cardano-tran…
Browse files Browse the repository at this point in the history
…slation-utilities

Feat/ogmios-cardano-translation-utilities
  • Loading branch information
rhyslbw authored Oct 21, 2022
2 parents 693e3a7 + b077cb7 commit bb3f3d1
Show file tree
Hide file tree
Showing 14 changed files with 1,040 additions and 30 deletions.
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"typescript": "^4.7.4"
},
"dependencies": {
"@cardano-ogmios/client": "5.5.2",
"@cardano-ogmios/client": "5.5.5",
"@cardano-sdk/util": "^0.5.0",
"@emurgo/cardano-serialization-lib-browser": "11.0.5",
"@emurgo/cardano-serialization-lib-nodejs": "11.0.5",
Expand Down
47 changes: 39 additions & 8 deletions packages/core/src/Cardano/types/Block.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { CSL } from '../..';
import { Ed25519PublicKey } from '.';
import { Hash28ByteBase16, Hash32ByteBase16, OpaqueString, typedBech32 } from '../util/primitives';
import { InvalidStringError } from '../../errors';
import { Lovelace } from './Value';
Expand Down Expand Up @@ -31,6 +33,7 @@ export type BlockId = Hash32ByteBase16<'BlockId'>;
export interface PartialBlockHeader {
blockNo: BlockNo;
slot: Slot;
/** Block header hash */
hash: BlockId;
}

Expand Down Expand Up @@ -74,18 +77,46 @@ export const SlotLeader = (value: string): SlotLeader => {
}
};

export interface Block {
/**
* Get Bech32 encoded VRF verification key from base64 encoded string
*
* @param value is a Base64 string
* @returns Bech32 encoded vrf_vk
*/
export const VrfVkBech32FromBase64 = (value: string) =>
VrfVkBech32(CSL.VRFVKey.from_bytes(Buffer.from(value, 'base64')).to_bech32('vrf_vk'));

/** Minimal Block type meant as a base for the more complete version `Block` */
// TODO: optionals (except previousBlock) are there because they are not calculated for Byron yet.
// Remove them once calculation is done and remove the Required<BlockMinimal> from interface Block
export interface BlockMinimal {
header: PartialBlockHeader;
/** Byron blocks fee not calculated yet */
fees?: Lovelace;
totalOutput: Lovelace;
txCount: number;
/** Byron blocks size not calculated yet */
size?: BlockSize;
previousBlock?: BlockId;
vrf?: VrfVkBech32;
/**
* This is the operational cold verification key of the stake pool
* Leaving as undefined for Byron blocks until we figure out how/if we can use the genesisKey field
*/
issuerVk?: Ed25519PublicKey;
}

export interface Block
extends Required<Omit<BlockMinimal, 'issuerVk' | 'previousBlock'>>,
Pick<BlockMinimal, 'previousBlock'> {
/**
* In case of blocks produced by BFT nodes, the SlotLeader the issuerVk hash
* For blocks produced by stake pools, it is the Bech32 encoded value of issuerVk hash
*/
slotLeader: SlotLeader;
date: Date;
epoch: EpochNo;
epochSlot: number;
slotLeader: SlotLeader;
size: BlockSize;
txCount: number;
totalOutput: Lovelace;
fees: Lovelace;
vrf: VrfVkBech32;
previousBlock?: BlockId;
nextBlock?: BlockId;
confirmations: number;
}
6 changes: 6 additions & 0 deletions packages/core/src/Ogmios/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as Ogmios from '@cardano-ogmios/client';

export * as ogmiosToCore from './ogmiosToCore';
export * as Ogmios from '@cardano-ogmios/client';
export { Schema } from '@cardano-ogmios/client';
export type OgmiosTypescriptLib = typeof Ogmios;
1 change: 1 addition & 0 deletions packages/core/src/Ogmios/ogmiosToCore/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ogmiosToCore';
171 changes: 171 additions & 0 deletions packages/core/src/Ogmios/ogmiosToCore/ogmiosToCore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { BigIntMath } from '@cardano-sdk/util';
import { Schema, isByronStandardBlock } from '@cardano-ogmios/client';

import { Cardano, Ogmios } from '../..';

type KeysOfUnion<T> = T extends T ? keyof T : never;
/**
* Ogmios has actual block under a property named like the era (e.g. `block.alonzo`).
* This type creates a union with all the properties. It is later used in
* [exhaustive switches](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking)
* to make sure all block types are handled and future blocks types will generate compile time errors.
*/
type BlockKind = KeysOfUnion<Schema.Block>;
type OgmiosBlockType =
| Schema.BlockAllegra
| Schema.BlockAlonzo
| Schema.BlockBabbage
| Schema.StandardBlock
| Schema.BlockMary
| Schema.BlockShelley;

type CommonBlock = Exclude<OgmiosBlockType, Schema.StandardBlock>;

interface Block<B extends OgmiosBlockType, T extends BlockKind> {
block: B;
kind: T;
}

type BlockAndKind =
| Block<Schema.BlockAllegra, 'allegra'>
| Block<Schema.BlockAlonzo, 'alonzo'>
| Block<Schema.BlockBabbage, 'babbage'>
| Block<Schema.StandardBlock, 'byron'>
| Block<Schema.BlockMary, 'mary'>
| Block<Schema.BlockShelley, 'shelley'>;

/**
* @returns
* - {BlockAndKind} that unlocks type narrowing in switch statements based on `kind`.
* Another advantage of using switch is using exhaustive check of case branches, making sure that future new
* block kinds will cause compilation errors, instead of silently fail or runtime errors.
* - `null` if `block` is the ByronEpochBoundaryBlock. This block can be skipped
*/
// eslint-disable-next-line complexity
const getBlockAndKind = (block: Schema.Block): BlockAndKind | null => {
let propName: BlockKind = 'alonzo';
if (Ogmios.isAllegraBlock(block)) propName = 'allegra';
if (Ogmios.isAlonzoBlock(block)) propName = 'alonzo';
if (Ogmios.isBabbageBlock(block)) propName = 'babbage';
if (Ogmios.isByronBlock(block)) propName = 'byron';
if (Ogmios.isMaryBlock(block)) propName = 'mary';
if (Ogmios.isShelleyBlock(block)) propName = 'shelley';

// If it complains because a branch is not handled, please add logic for the new block type.
switch (propName) {
case 'allegra':
return { block: (block as Schema.Allegra).allegra, kind: 'allegra' };
case 'alonzo':
return { block: (block as Schema.Alonzo).alonzo, kind: 'alonzo' };
case 'babbage':
return { block: (block as Schema.Babbage).babbage, kind: 'babbage' };
case 'byron':
// Return `null` if it is the EBB block to signal that it can be skipped
return isByronStandardBlock(block) ? { block: block.byron, kind: 'byron' } : null;
case 'mary':
return { block: (block as Schema.Mary).mary, kind: 'mary' };
case 'shelley':
return { block: (block as Schema.Shelley).shelley, kind: 'shelley' };
default: {
// will fail at compile time if not all branches are handled
// eslint-disable-next-line sonarjs/prefer-immediate-return
const _exhaustiveCheck: never = propName;
return _exhaustiveCheck;
}
}
};

// Mappers that apply to all Block types
const mapBlockHeight = (block: OgmiosBlockType): number => block.header.blockHeight;
const mapBlockSlot = (block: OgmiosBlockType): number => block.header.slot;
const mapPreviousBlock = (block: OgmiosBlockType): Cardano.BlockId => Cardano.BlockId(block.header.prevHash);

// Mappers specific to Byron block properties
const mapByronHash = (block: Schema.StandardBlock): Cardano.BlockId => Cardano.BlockId(block.hash);
const mapByronTotalOutputs = (block: Schema.StandardBlock): bigint =>
BigIntMath.sum(
block.body.txPayload.map(({ body: { outputs } }) => BigIntMath.sum(outputs.map(({ value: { coins } }) => coins)))
);
const mapByronTxCount = (block: Schema.StandardBlock): number => block.body.txPayload.length;

// Mappers for the rest of Block types
const mapCommonTxCount = (block: CommonBlock): number => block.body.length;
const mapCommonHash = (block: CommonBlock): Cardano.BlockId => Cardano.BlockId(block.headerHash);
const mapCommonTotalOutputs = (block: CommonBlock): Cardano.Lovelace =>
BigIntMath.sum(
block.body.map(({ body: { outputs } }) => BigIntMath.sum(outputs.map(({ value: { coins } }) => coins)))
);
const mapCommonBlockSize = (block: CommonBlock): number => block.header.blockSize;
const mapCommonFees = (block: CommonBlock): Cardano.Lovelace =>
block.body.map(({ body: { fee } }) => fee).reduce((prev, current) => prev + current, 0n);
// This is the VRF verification key, An Ed25519 verification key.
const mapCommonVrf = (block: CommonBlock): Cardano.VrfVkBech32 => Cardano.VrfVkBech32FromBase64(block.header.issuerVrf);
// SlotLeader is the producer pool id. It can be calculated from the issuer verification key
// which is actually the cold verification key
const mapCommonSlotLeader = (block: CommonBlock): Cardano.Ed25519PublicKey =>
Cardano.Ed25519PublicKey(block.header.issuerVk);

export const mapByronBlock = (block: Schema.StandardBlock): Cardano.BlockMinimal => ({
fees: undefined, // TODO: figure out how to calculate fees
header: {
blockNo: mapBlockHeight(block),
hash: mapByronHash(block),
slot: mapBlockSlot(block)
},
// TODO: use the genesisKey to provide a value here, but it needs more work. Leaving as undefined for now
issuerVk: undefined,
previousBlock: mapPreviousBlock(block),
// TODO: calculate byron blocksize by transforming into CSL Block object
size: undefined,
totalOutput: mapByronTotalOutputs(block),
txCount: mapByronTxCount(block),
vrf: undefined // no vrf key for byron. DbSync doesn't have one either
});

export const mapCommonBlock = (block: CommonBlock): Cardano.BlockMinimal => ({
fees: mapCommonFees(block),
header: {
blockNo: mapBlockHeight(block),
hash: mapCommonHash(block),
slot: mapBlockSlot(block)
},
issuerVk: mapCommonSlotLeader(block),
previousBlock: mapPreviousBlock(block),
size: mapCommonBlockSize(block),
totalOutput: mapCommonTotalOutputs(block),
txCount: mapCommonTxCount(block),
vrf: mapCommonVrf(block)
});

/**
* Translate `Ogmios` block to `Cardano.BlockMinimal`
*
* @param ogmiosBlock the block to translate into a `Cardano.BlockMinimal`
* @returns
* - {Cardano.BlockMinimal} a minimal block type encompassing information extracted from Ogmios block type.
* - `null` if `block` is the ByronEpochBoundaryBlock. This block can be skipped.
*/
export const getBlock = (ogmiosBlock: Schema.Block): Cardano.BlockMinimal | null => {
const b = getBlockAndKind(ogmiosBlock);
if (!b) return null;

switch (b.kind) {
case 'byron': {
return mapByronBlock(b.block);
}
case 'babbage':
case 'allegra':
case 'alonzo':
case 'mary':
case 'shelley': {
return mapCommonBlock(b.block);
}
default: {
// eslint-disable-next-line sonarjs/prefer-immediate-return
const _exhaustiveCheck: never = b;
return _exhaustiveCheck;
}
}
};

// byron-shelley-allegra-mary-alonzo-babbage
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './CSL';
export * from './util';
export * from './errors';
export * from './CardanoNode';
export * from './Ogmios';
133 changes: 133 additions & 0 deletions packages/core/test/Ogmios/ogmiosToCore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Cardano, ogmiosToCore } from '../../src';

import {
mockAllegraBlock,
mockAlonzoBlock,
mockBabbageBlock,
mockByronBlock,
mockMaryBlock,
mockShelleyBlock
} from './testData';

describe('ogmiosToCore', () => {
it('can translate from byron block', () => {
// using https://preprod.cardanoscan.io/block/42 as source of truth
expect(ogmiosToCore.getBlock(mockByronBlock)).toEqual(<Cardano.BlockMinimal>{
fees: undefined,
header: {
blockNo: 42,
hash: Cardano.BlockId('5c3103bd0ff5ea85a62b202a1d2500cf3ebe0b9d793ed09e7febfe27ef12c968'),
slot: 77_761
},
issuerVk: undefined,
previousBlock: Cardano.BlockId('dd8d7559a9b6c1177c0f5a328eb82967af68155d58cbcdc0a59de39a38aaf3f0'),
// got size: 626 by querying the postgres db populated by db-sync.
// Using size: undefined until we can calculate it
size: undefined,
totalOutput: 0n,
txCount: 0,
vrf: undefined
});
});

it('can translate from shelley block', () => {
// using https://preprod.cardanoscan.io/block/1087 as source of truth
expect(ogmiosToCore.getBlock(mockShelleyBlock)).toEqual(<Cardano.BlockMinimal>{
fees: 0n,
header: {
blockNo: 1087,
hash: Cardano.BlockId('071fceb6c20a412b9a9b57baedfe294e3cd9de641cd44c4cf8d0d56217e083ac'),
slot: 107_220
},
issuerVk: Cardano.Ed25519PublicKey('8b0960d234bda67d52432c5d1a26aca2bfb5b9a09f966d9592a7bf0c728a1ecd'),
previousBlock: Cardano.BlockId('8d5d930981710fc8c6ca9fc8e0628665283f7efb28c7e6bddeee2d289f012dee'),
// got size by querying the postgres db populated by db-sync
size: 3,
totalOutput: 0n,
txCount: 0,
// vrf from https://preprod.cexplorer.io/block/071fceb6c20a412b9a9b57baedfe294e3cd9de641cd44c4cf8d0d56217e083ac
vrf: Cardano.VrfVkBech32('vrf_vk15c2edf9h66wllthgvyttzhzwrngq0rvd0wchzqlw8qray60fq5usfngf29')
});
});

it('can translate from allegra block', () => {
// Verify data extracted from mock structure
const ogmiosBlock = mockAllegraBlock.allegra;
expect(ogmiosToCore.getBlock(mockAllegraBlock)).toEqual(<Cardano.BlockMinimal>{
fees: ogmiosBlock.body[0].body.fee,
header: {
blockNo: ogmiosBlock.header.blockHeight,
hash: Cardano.BlockId(ogmiosBlock.headerHash),
slot: ogmiosBlock.header.slot
},
issuerVk: Cardano.Ed25519PublicKey(ogmiosBlock.header.issuerVk),
previousBlock: Cardano.BlockId(ogmiosBlock.header.prevHash),
size: ogmiosBlock.header.blockSize,
totalOutput: 0n,
txCount: ogmiosBlock.body.length,
vrf: Cardano.VrfVkBech32FromBase64(ogmiosBlock.header.issuerVrf)
});
});

it('can translate from mary block', () => {
// Verify data extracted from mock structure
const ogmiosBlock = mockMaryBlock.mary;
expect(ogmiosToCore.getBlock(mockMaryBlock)).toEqual(<Cardano.BlockMinimal>{
fees: ogmiosBlock.body[0].body.fee + ogmiosBlock.body[1].body.fee,
header: {
blockNo: ogmiosBlock.header.blockHeight,
hash: Cardano.BlockId(ogmiosBlock.headerHash),
slot: ogmiosBlock.header.slot
},
issuerVk: Cardano.Ed25519PublicKey(ogmiosBlock.header.issuerVk),
previousBlock: Cardano.BlockId(ogmiosBlock.header.prevHash),
size: ogmiosBlock.header.blockSize,
totalOutput:
ogmiosBlock.body[0].body.outputs[0].value.coins +
ogmiosBlock.body[1].body.outputs[0].value.coins +
ogmiosBlock.body[1].body.outputs[1].value.coins,
txCount: ogmiosBlock.body.length,
vrf: Cardano.VrfVkBech32FromBase64(ogmiosBlock.header.issuerVrf)
});
});

it('can translate from alonzo block', () => {
// using https://preprod.cardanoscan.io/block/100000 as source of truth
expect(ogmiosToCore.getBlock(mockAlonzoBlock)).toEqual(<Cardano.BlockMinimal>{
fees: 202_549n,
header: {
blockNo: 100_000,
hash: Cardano.BlockId('514f8be63ef25c46bee47a90658977f815919c06222c0b480be1e29efbd72c49'),
slot: 5_481_752
},
issuerVk: Cardano.Ed25519PublicKey('a9d974fd26bfaf385749113f260271430276bed6ef4dad6968535de6778471ce'),

previousBlock: Cardano.BlockId('518a24a3fb0cc6ee1a31668a63994e4dbda70ede5ff13be494a3b4c1bb7709c8'),
// got size by querying the postgres db populated by db-sync
size: 836,
totalOutput: 8_287_924_709n,
txCount: 1,
// vrf from https://preprod.cexplorer.io/block/514f8be63ef25c46bee47a90658977f815919c06222c0b480be1e29efbd72c49
vrf: Cardano.VrfVkBech32('vrf_vk1p8s5ysf7dgsvfrw0p0q7zczdytkxc95zsq3p9sfshk9s3z86jfdql5fdft')
});
});

it('can translate from babbage block', () => {
// Verify data extracted from mock structure
const ogmiosBlock = mockBabbageBlock.babbage;
expect(ogmiosToCore.getBlock(mockBabbageBlock)).toEqual(<Cardano.BlockMinimal>{
fees: ogmiosBlock.body[0].body.fee,
header: {
blockNo: ogmiosBlock.header.blockHeight,
hash: Cardano.BlockId(ogmiosBlock.headerHash),
slot: ogmiosBlock.header.slot
},
issuerVk: Cardano.Ed25519PublicKey(ogmiosBlock.header.issuerVk),
previousBlock: Cardano.BlockId(ogmiosBlock.header.prevHash),
size: ogmiosBlock.header.blockSize,
totalOutput: ogmiosBlock.body[0].body.outputs[0].value.coins,
txCount: ogmiosBlock.body.length,
vrf: Cardano.VrfVkBech32FromBase64(ogmiosBlock.header.issuerVrf)
});
});
});
Loading

0 comments on commit bb3f3d1

Please sign in to comment.