2024-01-17 19:24:34 +00:00
|
|
|
## Nim-Codex
|
|
|
|
## Copyright (c) 2023 Status Research & Development GmbH
|
|
|
|
## Licensed under either of
|
|
|
|
## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
|
|
|
## * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
|
|
|
## at your option.
|
|
|
|
## This file may not be copied, modified, or distributed except according to
|
|
|
|
## those terms.
|
|
|
|
|
|
|
|
{.push raises: [].}
|
|
|
|
|
|
|
|
import std/math
|
|
|
|
import std/sequtils
|
|
|
|
import std/sugar
|
|
|
|
|
|
|
|
import pkg/libp2p
|
|
|
|
import pkg/chronos
|
|
|
|
import pkg/questionable
|
|
|
|
import pkg/questionable/results
|
2024-02-08 02:27:11 +00:00
|
|
|
import pkg/constantine/math/io/io_fields
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-03-19 03:25:13 +00:00
|
|
|
import ../../logutils
|
2024-02-08 02:27:11 +00:00
|
|
|
import ../../utils
|
2024-01-17 19:24:34 +00:00
|
|
|
import ../../stores
|
|
|
|
import ../../manifest
|
2024-02-08 02:27:11 +00:00
|
|
|
import ../../merkletree
|
2024-01-17 19:24:34 +00:00
|
|
|
import ../../utils/digest
|
2024-02-08 02:27:11 +00:00
|
|
|
import ../../utils/asynciter
|
|
|
|
import ../../indexingstrategy
|
|
|
|
|
2024-01-17 19:24:34 +00:00
|
|
|
import ../converters
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
export converters, asynciter
|
2024-01-17 19:24:34 +00:00
|
|
|
|
|
|
|
logScope:
|
|
|
|
topics = "codex slotsbuilder"
|
|
|
|
|
|
|
|
type
|
2024-02-08 02:27:11 +00:00
|
|
|
SlotsBuilder*[T, H] = ref object of RootObj
|
2024-01-17 19:24:34 +00:00
|
|
|
store: BlockStore
|
2024-02-08 02:27:11 +00:00
|
|
|
manifest: Manifest # current manifest
|
|
|
|
strategy: IndexingStrategy # indexing strategy
|
|
|
|
cellSize: NBytes # cell size
|
|
|
|
numSlotBlocks: Natural # number of blocks per slot (should yield a power of two number of cells)
|
|
|
|
slotRoots: seq[H] # roots of the slots
|
|
|
|
emptyBlock: seq[byte] # empty block
|
|
|
|
verifiableTree: ?T # verification tree (dataset tree)
|
|
|
|
emptyDigestTree: T # empty digest tree for empty blocks
|
|
|
|
|
|
|
|
func verifiable*[T, H](self: SlotsBuilder[T, H]): bool {.inline.} =
|
|
|
|
## Returns true if the slots are verifiable.
|
|
|
|
##
|
|
|
|
|
|
|
|
self.manifest.verifiable
|
|
|
|
|
|
|
|
func slotRoots*[T, H](self: SlotsBuilder[T, H]): seq[H] {.inline.} =
|
2024-01-17 19:24:34 +00:00
|
|
|
## Returns the slot roots.
|
|
|
|
##
|
|
|
|
|
|
|
|
self.slotRoots
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
func verifyTree*[T, H](self: SlotsBuilder[T, H]): ?T {.inline.} =
|
2024-01-17 19:24:34 +00:00
|
|
|
## Returns the slots tree (verification tree).
|
|
|
|
##
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
self.verifiableTree
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
func verifyRoot*[T, H](self: SlotsBuilder[T, H]): ?H {.inline.} =
|
2024-01-17 19:24:34 +00:00
|
|
|
## Returns the slots root (verification root).
|
|
|
|
##
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
if tree =? self.verifyTree and root =? tree.root:
|
|
|
|
return some root
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
func numSlots*[T, H](self: SlotsBuilder[T, H]): Natural =
|
2024-01-17 19:24:34 +00:00
|
|
|
## Number of slots.
|
|
|
|
##
|
|
|
|
|
|
|
|
self.manifest.numSlots
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
func numSlotBlocks*[T, H](self: SlotsBuilder[T, H]): Natural =
|
2024-01-17 19:24:34 +00:00
|
|
|
## Number of blocks per slot.
|
|
|
|
##
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
self.numSlotBlocks
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
func numBlocks*[T, H](self: SlotsBuilder[T, H]): Natural =
|
|
|
|
## Number of blocks.
|
|
|
|
##
|
|
|
|
|
|
|
|
self.numSlotBlocks * self.manifest.numSlots
|
|
|
|
|
|
|
|
func slotBytes*[T, H](self: SlotsBuilder[T, H]): NBytes =
|
2024-01-17 19:24:34 +00:00
|
|
|
## Number of bytes per slot.
|
|
|
|
##
|
|
|
|
|
|
|
|
(self.manifest.blockSize.int * self.numSlotBlocks).NBytes
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
func numBlockCells*[T, H](self: SlotsBuilder[T, H]): Natural =
|
2024-01-17 19:24:34 +00:00
|
|
|
## Number of cells per block.
|
|
|
|
##
|
|
|
|
|
|
|
|
(self.manifest.blockSize div self.cellSize).Natural
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
func cellSize*[T, H](self: SlotsBuilder[T, H]): NBytes =
|
2024-01-17 19:24:34 +00:00
|
|
|
## Cell size.
|
|
|
|
##
|
|
|
|
|
|
|
|
self.cellSize
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
func numSlotCells*[T, H](self: SlotsBuilder[T, H]): Natural =
|
2024-01-17 19:24:34 +00:00
|
|
|
## Number of cells per slot.
|
|
|
|
##
|
|
|
|
|
|
|
|
self.numBlockCells * self.numSlotBlocks
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
func slotIndiciesIter*[T, H](self: SlotsBuilder[T, H], slot: Natural): ?!Iter[int] =
|
2024-01-17 19:24:34 +00:00
|
|
|
## Returns the slot indices.
|
|
|
|
##
|
|
|
|
|
|
|
|
self.strategy.getIndicies(slot).catch
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
func slotIndicies*[T, H](self: SlotsBuilder[T, H], slot: Natural): seq[int] =
|
2024-01-17 19:24:34 +00:00
|
|
|
## Returns the slot indices.
|
|
|
|
##
|
|
|
|
|
|
|
|
if iter =? self.strategy.getIndicies(slot).catch:
|
2024-02-08 02:27:11 +00:00
|
|
|
return toSeq(iter)
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
func manifest*[T, H](self: SlotsBuilder[T, H]): Manifest =
|
2024-01-17 19:24:34 +00:00
|
|
|
## Returns the manifest.
|
|
|
|
##
|
|
|
|
|
|
|
|
self.manifest
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
proc buildBlockTree*[T, H](
|
|
|
|
self: SlotsBuilder[T, H],
|
|
|
|
blkIdx: Natural,
|
|
|
|
slotPos: Natural): Future[?!(seq[byte], T)] {.async.} =
|
|
|
|
## Build the block digest tree and return a tuple with the
|
|
|
|
## block data and the tree.
|
|
|
|
##
|
|
|
|
|
|
|
|
logScope:
|
|
|
|
blkIdx = blkIdx
|
|
|
|
slotPos = slotPos
|
|
|
|
numSlotBlocks = self.manifest.numSlotBlocks
|
|
|
|
cellSize = self.cellSize
|
|
|
|
|
|
|
|
trace "Building block tree"
|
|
|
|
|
|
|
|
if slotPos > (self.manifest.numSlotBlocks - 1):
|
|
|
|
# pad blocks are 0 byte blocks
|
|
|
|
trace "Returning empty digest tree for pad block"
|
|
|
|
return success (self.emptyBlock, self.emptyDigestTree)
|
|
|
|
|
2024-01-17 19:24:34 +00:00
|
|
|
without blk =? await self.store.getBlock(self.manifest.treeCid, blkIdx), err:
|
2024-02-08 02:27:11 +00:00
|
|
|
error "Failed to get block CID for tree at index", err = err.msg
|
2024-01-17 19:24:34 +00:00
|
|
|
return failure(err)
|
|
|
|
|
|
|
|
if blk.isEmpty:
|
2024-02-08 02:27:11 +00:00
|
|
|
success (self.emptyBlock, self.emptyDigestTree)
|
2024-01-17 19:24:34 +00:00
|
|
|
else:
|
2024-10-28 14:35:40 +00:00
|
|
|
without tree =? (await T.digestTree(blk.data, self.cellSize.int)), err:
|
2024-02-08 02:27:11 +00:00
|
|
|
error "Failed to create digest for block", err = err.msg
|
2024-01-17 19:24:34 +00:00
|
|
|
return failure(err)
|
|
|
|
|
|
|
|
success (blk.data, tree)
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
proc getCellHashes*[T, H](
|
|
|
|
self: SlotsBuilder[T, H],
|
|
|
|
slotIndex: Natural): Future[?!seq[H]] {.async.} =
|
|
|
|
## Collect all the cells from a block and return
|
|
|
|
## their hashes.
|
|
|
|
##
|
2024-01-17 19:24:34 +00:00
|
|
|
|
|
|
|
let
|
|
|
|
treeCid = self.manifest.treeCid
|
|
|
|
blockCount = self.manifest.blocksCount
|
|
|
|
numberOfSlots = self.manifest.numSlots
|
|
|
|
|
|
|
|
logScope:
|
|
|
|
treeCid = treeCid
|
2024-02-08 02:27:11 +00:00
|
|
|
origBlockCount = blockCount
|
2024-01-17 19:24:34 +00:00
|
|
|
numberOfSlots = numberOfSlots
|
|
|
|
slotIndex = slotIndex
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
let hashes = collect(newSeq):
|
|
|
|
for i, blkIdx in self.strategy.getIndicies(slotIndex):
|
|
|
|
logScope:
|
|
|
|
blkIdx = blkIdx
|
|
|
|
pos = i
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
trace "Getting block CID for tree at index"
|
|
|
|
without (_, tree) =? (await self.buildBlockTree(blkIdx, i)) and
|
|
|
|
digest =? tree.root, err:
|
|
|
|
error "Failed to get block CID for tree at index", err = err.msg
|
|
|
|
return failure(err)
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
trace "Get block digest", digest = digest.toHex
|
|
|
|
digest
|
2024-01-17 19:24:34 +00:00
|
|
|
|
|
|
|
success hashes
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
proc buildSlotTree*[T, H](
|
|
|
|
self: SlotsBuilder[T, H],
|
|
|
|
slotIndex: Natural): Future[?!T] {.async.} =
|
|
|
|
## Build the slot tree from the block digest hashes
|
|
|
|
## and return the tree.
|
|
|
|
|
|
|
|
without cellHashes =? (await self.getCellHashes(slotIndex)), err:
|
2024-01-17 19:24:34 +00:00
|
|
|
error "Failed to select slot blocks", err = err.msg
|
|
|
|
return failure(err)
|
|
|
|
|
2024-10-28 14:35:40 +00:00
|
|
|
await T.init(cellHashes)
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
proc buildSlot*[T, H](
|
|
|
|
self: SlotsBuilder[T, H],
|
|
|
|
slotIndex: Natural): Future[?!H] {.async.} =
|
|
|
|
## Build a slot tree and store the proofs in
|
|
|
|
## the block store.
|
2024-01-17 19:24:34 +00:00
|
|
|
##
|
|
|
|
|
|
|
|
logScope:
|
|
|
|
cid = self.manifest.treeCid
|
|
|
|
slotIndex = slotIndex
|
|
|
|
|
|
|
|
trace "Building slot tree"
|
|
|
|
|
|
|
|
without tree =? (await self.buildSlotTree(slotIndex)) and
|
|
|
|
treeCid =? tree.root.?toSlotCid, err:
|
|
|
|
error "Failed to build slot tree", err = err.msg
|
|
|
|
return failure(err)
|
|
|
|
|
|
|
|
trace "Storing slot tree", treeCid, slotIndex, leaves = tree.leavesCount
|
|
|
|
for i, leaf in tree.leaves:
|
|
|
|
without cellCid =? leaf.toCellCid, err:
|
|
|
|
error "Failed to get CID for slot cell", err = err.msg
|
|
|
|
return failure(err)
|
|
|
|
|
|
|
|
without proof =? tree.getProof(i) and
|
|
|
|
encodableProof =? proof.toEncodableProof, err:
|
|
|
|
error "Failed to get proof for slot tree", err = err.msg
|
|
|
|
return failure(err)
|
|
|
|
|
|
|
|
if err =? (await self.store.putCidAndProof(
|
|
|
|
treeCid, i, cellCid, encodableProof)).errorOption:
|
|
|
|
error "Failed to store slot tree", err = err.msg
|
|
|
|
return failure(err)
|
|
|
|
|
|
|
|
tree.root()
|
|
|
|
|
2024-10-28 14:35:40 +00:00
|
|
|
proc buildVerifyTree*[T, H](self: SlotsBuilder[T, H], slotRoots: seq[H]): Future[?!T] {.async.} =
|
|
|
|
await T.init(@slotRoots)
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
proc buildSlots*[T, H](self: SlotsBuilder[T, H]): Future[?!void] {.async.} =
|
2024-01-17 19:24:34 +00:00
|
|
|
## Build all slot trees and store them in the block store.
|
|
|
|
##
|
|
|
|
|
|
|
|
logScope:
|
|
|
|
cid = self.manifest.treeCid
|
|
|
|
blockCount = self.manifest.blocksCount
|
|
|
|
|
|
|
|
trace "Building slots"
|
|
|
|
|
|
|
|
if self.slotRoots.len == 0:
|
|
|
|
self.slotRoots = collect(newSeq):
|
|
|
|
for i in 0..<self.manifest.numSlots:
|
|
|
|
without slotRoot =? (await self.buildSlot(i)), err:
|
|
|
|
error "Failed to build slot", err = err.msg, index = i
|
|
|
|
return failure(err)
|
|
|
|
slotRoot
|
|
|
|
|
2024-10-28 14:35:40 +00:00
|
|
|
without tree =? (await self.buildVerifyTree(self.slotRoots)) and root =? tree.root, err:
|
2024-01-17 19:24:34 +00:00
|
|
|
error "Failed to build slot roots tree", err = err.msg
|
|
|
|
return failure(err)
|
|
|
|
|
|
|
|
if verifyTree =? self.verifyTree and verifyRoot =? verifyTree.root:
|
2024-02-08 02:27:11 +00:00
|
|
|
if not bool(verifyRoot == root): # TODO: `!=` doesn't work for SecretBool
|
2024-01-17 19:24:34 +00:00
|
|
|
return failure "Existing slots root doesn't match reconstructed root."
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
self.verifiableTree = some tree
|
2024-01-17 19:24:34 +00:00
|
|
|
|
|
|
|
success()
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
proc buildManifest*[T, H](self: SlotsBuilder[T, H]): Future[?!Manifest] {.async.} =
|
2024-01-17 19:24:34 +00:00
|
|
|
if err =? (await self.buildSlots()).errorOption:
|
|
|
|
error "Failed to build slot roots", err = err.msg
|
|
|
|
return failure(err)
|
|
|
|
|
|
|
|
without rootCids =? self.slotRoots.toSlotCids(), err:
|
|
|
|
error "Failed to map slot roots to CIDs", err = err.msg
|
|
|
|
return failure(err)
|
|
|
|
|
|
|
|
without rootProvingCidRes =? self.verifyRoot.?toVerifyCid() and
|
|
|
|
rootProvingCid =? rootProvingCidRes, err: # TODO: why doesn't `.?` unpack the result?
|
|
|
|
error "Failed to map slot roots to CIDs", err = err.msg
|
|
|
|
return failure(err)
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
Manifest.new(
|
|
|
|
self.manifest,
|
|
|
|
rootProvingCid,
|
|
|
|
rootCids,
|
|
|
|
self.cellSize,
|
|
|
|
self.strategy.strategyType)
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-10-28 14:35:40 +00:00
|
|
|
proc init*[T, H](self: SlotsBuilder[T, H]): Future[?!void] {.async.} =
|
|
|
|
without emptyTree =? (await T.digestTree(self.emptyBlock, self.cellSize.int)), err:
|
|
|
|
return failure err
|
|
|
|
self.emptyDigestTree = emptyTree
|
|
|
|
|
|
|
|
if self.manifest.verifiable:
|
|
|
|
without tree =? (await self.buildVerifyTree(self.slotRoots)), err:
|
|
|
|
return failure err
|
|
|
|
|
|
|
|
without verifyRoot =? tree.root, err:
|
|
|
|
return failure err
|
|
|
|
|
|
|
|
without expectedRoot =? self.manifest.verifyRoot.fromVerifyCid(), err:
|
|
|
|
return failure err
|
|
|
|
|
|
|
|
if verifyRoot != expectedRoot:
|
|
|
|
return failure "Existing slots root doesn't match reconstructed root."
|
|
|
|
|
|
|
|
self.verifiableTree = some tree
|
|
|
|
|
|
|
|
trace "Slots builder initialized"
|
2024-10-29 09:00:33 +00:00
|
|
|
success()
|
2024-10-28 14:35:40 +00:00
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
proc new*[T, H](
|
|
|
|
_: type SlotsBuilder[T, H],
|
2024-01-17 19:24:34 +00:00
|
|
|
store: BlockStore,
|
|
|
|
manifest: Manifest,
|
2024-02-08 02:27:11 +00:00
|
|
|
strategy = SteppedStrategy,
|
|
|
|
cellSize = DefaultCellSize): ?!SlotsBuilder[T, H] =
|
2024-01-17 19:24:34 +00:00
|
|
|
|
|
|
|
if not manifest.protected:
|
2024-02-08 02:27:11 +00:00
|
|
|
trace "Manifest is not protected."
|
|
|
|
return failure("Manifest is not protected.")
|
|
|
|
|
|
|
|
logScope:
|
|
|
|
blockSize = manifest.blockSize
|
|
|
|
strategy = strategy
|
|
|
|
cellSize = cellSize
|
2024-01-17 19:24:34 +00:00
|
|
|
|
|
|
|
if (manifest.blocksCount mod manifest.numSlots) != 0:
|
2024-02-08 02:27:11 +00:00
|
|
|
trace "Number of blocks must be divisable by number of slots."
|
2024-01-17 19:24:34 +00:00
|
|
|
return failure("Number of blocks must be divisable by number of slots.")
|
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
let cellSize = if manifest.verifiable: manifest.cellSize else: cellSize
|
2024-01-17 19:24:34 +00:00
|
|
|
if (manifest.blockSize mod cellSize) != 0.NBytes:
|
2024-02-08 02:27:11 +00:00
|
|
|
trace "Block size must be divisable by cell size."
|
2024-01-17 19:24:34 +00:00
|
|
|
return failure("Block size must be divisable by cell size.")
|
|
|
|
|
|
|
|
let
|
2024-02-19 02:19:59 +00:00
|
|
|
numSlotBlocks = manifest.numSlotBlocks
|
|
|
|
numBlockCells = (manifest.blockSize div cellSize).int # number of cells per block
|
|
|
|
numSlotCells = manifest.numSlotBlocks * numBlockCells # number of uncorrected slot cells
|
|
|
|
pow2SlotCells = nextPowerOfTwo(numSlotCells) # pow2 cells per slot
|
|
|
|
numPadSlotBlocks = (pow2SlotCells div numBlockCells) - numSlotBlocks # pow2 blocks per slot
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-02-19 02:19:59 +00:00
|
|
|
numSlotBlocksTotal = # pad blocks per slot
|
|
|
|
if numPadSlotBlocks > 0:
|
|
|
|
numPadSlotBlocks + numSlotBlocks
|
|
|
|
else:
|
|
|
|
numSlotBlocks
|
|
|
|
|
|
|
|
numBlocksTotal = numSlotBlocksTotal * manifest.numSlots # number of blocks per slot
|
|
|
|
|
|
|
|
emptyBlock = newSeq[byte](manifest.blockSize.int)
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
strategy = ? strategy.init(
|
|
|
|
0,
|
2024-02-19 02:19:59 +00:00
|
|
|
numBlocksTotal - 1,
|
2024-02-08 02:27:11 +00:00
|
|
|
manifest.numSlots).catch
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
logScope:
|
2024-02-19 02:19:59 +00:00
|
|
|
numSlotBlocks = numSlotBlocks
|
|
|
|
numBlockCells = numBlockCells
|
|
|
|
numSlotCells = numSlotCells
|
|
|
|
pow2SlotCells = pow2SlotCells
|
|
|
|
numPadSlotBlocks = numPadSlotBlocks
|
|
|
|
numBlocksTotal = numBlocksTotal
|
|
|
|
numSlotBlocksTotal = numSlotBlocksTotal
|
|
|
|
strategy = strategy.strategyType
|
2024-02-08 02:27:11 +00:00
|
|
|
|
|
|
|
trace "Creating slots builder"
|
|
|
|
|
|
|
|
var
|
|
|
|
self = SlotsBuilder[T, H](
|
|
|
|
store: store,
|
|
|
|
manifest: manifest,
|
|
|
|
strategy: strategy,
|
|
|
|
cellSize: cellSize,
|
|
|
|
emptyBlock: emptyBlock,
|
2024-10-28 14:35:40 +00:00
|
|
|
numSlotBlocks: numSlotBlocksTotal)
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-02-08 02:27:11 +00:00
|
|
|
if manifest.verifiable:
|
|
|
|
if manifest.slotRoots.len == 0 or
|
|
|
|
manifest.slotRoots.len != manifest.numSlots:
|
|
|
|
return failure "Manifest is verifiable but slot roots are missing or invalid."
|
2024-01-17 19:24:34 +00:00
|
|
|
|
2024-10-28 14:35:40 +00:00
|
|
|
self.slotRoots = self.manifest.slotRoots.mapIt( (? it.fromSlotCid() ))
|
2024-01-17 19:24:34 +00:00
|
|
|
|
|
|
|
success self
|