diff --git a/tvm/src/boc/BagOfCells.kt b/tvm/src/boc/BagOfCells.kt index 94b18b47..2c981e91 100644 --- a/tvm/src/boc/BagOfCells.kt +++ b/tvm/src/boc/BagOfCells.kt @@ -1,3 +1,5 @@ +@file:Suppress("PackageDirectoryMismatch") + package org.ton.boc import io.ktor.utils.io.core.* diff --git a/tvm/src/boc/BagOfCellsImpl.kt b/tvm/src/boc/BagOfCellsImpl.kt index a8572e54..e5e7921b 100644 --- a/tvm/src/boc/BagOfCellsImpl.kt +++ b/tvm/src/boc/BagOfCellsImpl.kt @@ -1,3 +1,5 @@ +@file:Suppress("PackageDirectoryMismatch") + package org.ton.boc import io.ktor.utils.io.core.* diff --git a/tvm/src/boc/BagOfCellsUtils.kt b/tvm/src/boc/BagOfCellsUtils.kt index da1f1d63..ade617e7 100644 --- a/tvm/src/boc/BagOfCellsUtils.kt +++ b/tvm/src/boc/BagOfCellsUtils.kt @@ -1,3 +1,5 @@ +@file:Suppress("PackageDirectoryMismatch") + package org.ton.boc import io.ktor.utils.io.core.* diff --git a/tvm/src/boc/CachedBagOfCells.kt b/tvm/src/boc/CachedBagOfCells.kt index 0f2ecbe3..69872df7 100644 --- a/tvm/src/boc/CachedBagOfCells.kt +++ b/tvm/src/boc/CachedBagOfCells.kt @@ -1,3 +1,5 @@ +@file:Suppress("PackageDirectoryMismatch") + package org.ton.boc import io.ktor.utils.io.core.* diff --git a/tvm/src/cell/BagOfCells.kt b/tvm/src/cell/BagOfCells.kt new file mode 100644 index 00000000..0a71c374 --- /dev/null +++ b/tvm/src/cell/BagOfCells.kt @@ -0,0 +1,266 @@ +@file:Suppress("PackageDirectoryMismatch") + +package org.ton.kotlin.cell + +import kotlinx.io.Buffer +import kotlinx.io.buffered +import kotlinx.io.bytestring.ByteString +import kotlinx.io.readByteString +import org.ton.bitstring.BitString +import org.ton.cell.Cell +import org.ton.cell.CellBuilder +import org.ton.cell.CellDescriptor +import org.ton.cell.DataCell +import org.ton.kotlin.cell.internal.ByteArrayRandomAccessStorage +import org.ton.kotlin.cell.internal.ByteStringRandomAccessSource +import org.ton.kotlin.cell.internal.RandomAccessSource +import org.ton.kotlin.cell.internal.readLong + +public sealed interface BagOfCells : CellContext { + public val header: BagOfCellsHeader + + public fun getRootCell(index: Int = 0): Cell +} + +public fun BagOfCells(byteArray: ByteArray): BagOfCells = StaticBagOfCells(byteArray) +public fun BagOfCells(byteString: ByteString): BagOfCells = StaticBagOfCells(byteString) + +public val BagOfCells.rootCount: Int get() = header.rootCount + +public operator fun BagOfCells.get(index: Int): Cell = getRootCell(index) + +public class StaticBagOfCells internal constructor( + private val source: RandomAccessSource +) : BagOfCells, CellContext { + public constructor(byteArray: ByteArray) : this(ByteArrayRandomAccessStorage(byteArray)) + public constructor(byteString: ByteString) : this(ByteStringRandomAccessSource(byteString)) + + public override val header: BagOfCellsHeader by lazy { + readHeader() + } + private val cachedCells = mutableMapOf() + private val cachedDescriptors = ArrayList>() + + public override fun getRootCell(index: Int): Cell { + val cellIndex = loadRootIndex(index) + val dataCell = loadDataCell(cellIndex) + return dataCell + } + + private fun loadDataCell(index: Int): DataCell { + cachedCells[index]?.let { + return it + } + + val (descriptor, cellLocation) = cachedDescriptors.getOrElse(index) { + val cellLocation = getCellLocation(index) + val buffer = Buffer() + source.position = cellLocation.start + source.readAtMostTo(buffer, 2) + + val d1 = buffer.readByte() + val d2 = buffer.readByte() + CellDescriptor(d1, d2) to cellLocation + } + + source.position = cellLocation.start + 2 + val buffer = source.buffered() + cachedDescriptors.add(Pair(descriptor, cellLocation)) + if (descriptor.isAbsent) { + throw IllegalStateException("Cell is absent") + } + val hashCount = descriptor.levelMask.hashCount + val hasHashes = descriptor.hasHashes + + val hashes = List(if (hasHashes) hashCount else 0) { + buffer.readByteString(Cell.HASH_BYTES) + } + val depths = List(if (hasHashes) hashCount else 0) { + buffer.readShort() + } + val dataLength = descriptor.dataLength + val rawData = ByteArray(dataLength + if (descriptor.isAligned) 1 else 0) + buffer.readAtMostTo(rawData, 0, dataLength) + if (descriptor.isAligned) { + rawData[dataLength] = 0b1000_0000.toByte() + } + val bits = BitString(rawData) + val refsCount = descriptor.referenceCount + val refsIndexes = List(refsCount) { + val refIndex = buffer.readLong(header.refByteSize).toInt() + if (refIndex >= header.cellCount) { + throw IllegalStateException("Invalid BOC cell #$index refers ($it) to cell #$refIndex which too big, cellCount=${header.cellCount}") + } + if (index >= refIndex) { + throw IllegalStateException("Invalid BOC cell #$index refers ($it) to cell #$refIndex which is backward reference") + } + refIndex + } + + val references = refsIndexes.map { + when (val ref = loadAnyCell(it)) { + is DataCell -> BocLoadedCell(it, ref) + is ExternalCell -> BocCell(it, ref) + else -> ref + } + } + + val cell = DataCell(descriptor, hashes, depths, bits, references) +// val cell = DataCell.create(descriptor, data, refs) +// if (hasHashes) { +// check(cell.hashes == hashes) { +// "Invalid hashes: provided: $hashes, calculated: ${cell.hashes}" +// } +// check(cell.depths == depths) { +// "Invalid depths: provided: $depths, calculated: ${cell.depths}" +// } +// } + + if (cellLocation.shouldCache) { + cachedCells[index] = cell + } + return cell + } + + public fun loadAnyCell(index: Int): Cell { + cachedCells[index]?.let { + return it + } + + val cellLocation = getCellLocation(index) + val buffer = Buffer() + source.position = cellLocation.start + source.readAtMostTo(buffer, cellLocation.end - cellLocation.start) + + val descriptor = CellDescriptor(buffer.readByte(), buffer.readByte()) + if (!descriptor.hasHashes) { + return loadDataCell(index) + } + + val hashCount = descriptor.levelMask.hashCount + val hashes = List(hashCount) { + buffer.readByteString(Cell.HASH_BYTES) + } + val depths = List(hashCount) { + buffer.readShort() + } + + return ExternalCell(descriptor, hashes, depths, index) + } + + private fun getCellLocation(index: Int): CellLocation { + require(index in 0 until header.cellCount) { + "Invalid cell index: $index, cellCount=${header.cellCount}" + } + if (!header.hasIndex) { + println("Warning: BOC does not have index, using linear search for cell #$index") + if (index < cachedDescriptors.size) { + return cachedDescriptors[index].second + } + var lastLocation = cachedDescriptors.lastOrNull()?.second + val buffer = Buffer() + for (i in cachedDescriptors.size..index) { + source.position = lastLocation?.end ?: header.dataOffset + source.readAtMostTo(buffer, 2) + val d1 = buffer.readByte() + val d2 = buffer.readByte() + val descriptor = CellDescriptor(d1, d2) + val endOffset = descriptor.bytesCount(header.refByteSize) + val location = CellLocation(source.position - 2, source.position + endOffset, false) + cachedDescriptors.add(descriptor to location) + lastLocation = location + } + return lastLocation ?: error("Failed to found cell location for #$index") + } + + + var start = loadIndexOffset(index - 1) + var end = loadIndexOffset(index) + var shouldCache = true + if (header.hasCacheBits) { + shouldCache = (end and 1) != 0L + start = start ushr 1 + end = end ushr 1 + } + val dataOffset = header.dataOffset + start += dataOffset + end += dataOffset + return CellLocation(start, end, shouldCache) + } + + private fun loadIndexOffset(index: Int): Long { + if (index < 0) return 0L + val buffer = Buffer() + if (header.hasIndex) { + source.position = header.indexOffset + (index.toLong() * header.offsetByteSize) + source.readAtMostTo(buffer, header.offsetByteSize.toLong()) + } else { + throw IllegalStateException("Searching for index in a BOC without index") + } + return buffer.readLong(header.offsetByteSize) + } + + private fun loadRootIndex(rootIndex: Int): Int { + require(rootIndex in 0..rootCount) { + "Invalid root index: $rootIndex, rootCount=$rootCount" + } + if (!header.hasRoots) { + return 0 + } + val buffer = Buffer() + source.position = header.rootsOffset + (rootIndex.toLong() * header.refByteSize) + source.readAtMostTo(buffer, header.refByteSize.toLong()) + return buffer.readLong(header.refByteSize).toInt() + } + + @OptIn(ExperimentalStdlibApi::class) + private fun readHeader(): BagOfCellsHeader { + source.position = 0 + return BagOfCellsHeader.parse(source.buffered()) + } + + override fun toString(): String = "StaticBagOfCells(header=$header)" + + override fun loadCell(cell: Cell): DataCell { + return when (cell) { + is DataCell -> cell + is BocCell -> loadCell(cell.cell) + is BocLoadedCell -> loadCell(cell.cell) + is ExternalCell -> loadDataCell(cell.index) + else -> throw IllegalArgumentException("Unsupported cell type: $cell") + } + } + + override fun finalizeCell(builder: CellBuilder): Cell { + TODO("Not yet implemented") + } + + private class BocCell( + val index: Int, + val cell: Cell, + ) : Cell by cell { + override fun toString(): String = "$index: $cell" + } + + private class BocLoadedCell( + val index: Int, + val cell: LoadedCell, + ) : LoadedCell by cell { + override fun toString(): String = "$index: $cell" + } + + private data class CellLocation( + val start: Long, + val end: Long, + val shouldCache: Boolean + ) + + private fun CellDescriptor.bytesCount(refByteSize: Int): Int { + val n = hashCount + val hasHashes = hasHashes + val depthOffset = if (hasHashes) n * Cell.HASH_BYTES else 0 + val dataOffset = depthOffset + if (hasHashes) n * Cell.DEPTH_BYTES else 0 + val refsOffset = dataOffset + dataLength + return refsOffset + referenceCount * refByteSize + } +} diff --git a/tvm/src/cell/BagOfCellsHeader.kt b/tvm/src/cell/BagOfCellsHeader.kt new file mode 100644 index 00000000..acae390a --- /dev/null +++ b/tvm/src/cell/BagOfCellsHeader.kt @@ -0,0 +1,121 @@ +@file:Suppress("PackageDirectoryMismatch") + +package org.ton.kotlin.cell + +import kotlinx.io.Source +import org.ton.kotlin.cell.internal.readLong + +public data class BagOfCellsHeader( + val magic: Int, + val rootCount: Int, + val cellCount: Int, + val absentCount: Int, + val refByteSize: Int, + val offsetByteSize: Int, + val hasIndex: Boolean, + val hasRoots: Boolean, + val hasCrc32c: Boolean, + val hasCacheBits: Boolean, + val rootsOffset: Long, + val indexOffset: Long, + val dataOffset: Long, + val dataSize: Long, + val totalSize: Long +) { + public companion object { + public const val BOC_GENERIC_MAGIC: Int = 0xB5EE9C72.toInt() + public const val BOC_INDEXED_MAGIC: Int = 0x68FF65F3 + public const val BOC_INDEXED_CRC32C_MAGIC: Int = 0xACC3A728.toInt() + + public fun parse(source: Source): BagOfCellsHeader { + val magic = source.readInt() + val byte = source.readByte().toInt() and 0xFF + + val hasIndex: Boolean + val hasCrc32c: Boolean + val hasCacheBits: Boolean + if (magic == BOC_GENERIC_MAGIC) { + hasIndex = (byte and 0b1000_0000) != 0 + hasCrc32c = (byte and 0b0100_0000) != 0 + hasCacheBits = (byte and 0b0010_0000) != 0 + } else { + hasIndex = false + hasCrc32c = false + hasCacheBits = false + } + check(!hasCacheBits || hasIndex) { + "Invalid BOC ${magic.toHexString()} header: hasCacheBits=$hasCacheBits, hasIndex=$hasIndex" + } + + val refByteSize = byte and 0b0001_1111 + check(refByteSize in 1..4) { + "Invalid BOC header: refByteSize=$refByteSize" + } + + val offsetByteSize = source.readByte().toInt() + check(offsetByteSize in 1..8) { + "Invalid BOC header: offsetByteSize=$offsetByteSize" + } + val rootsOffset = 6L + 3 * refByteSize + offsetByteSize + + val cellCount = source.readLong(refByteSize).toInt() + check(cellCount >= 0) { + "Invalid BOC header: cellCount=$cellCount" + } + + val rootCount = source.readLong(refByteSize).toInt() + check(rootCount > 0) { + "Invalid BOC header: rootCount=$rootCount" + } + var indexOffset = rootsOffset + var hasRoots = false + if (magic == BOC_GENERIC_MAGIC) { + indexOffset += rootCount.toLong() * refByteSize + hasRoots = true + } else { + check(rootCount == 1) { + "Invalid BOC ${magic.toHexString()} header: rootCount=$rootCount" + } + } + var dataOffset = indexOffset + if (hasIndex) { + dataOffset += cellCount * offsetByteSize + } + + val absentCount = source.readLong(refByteSize).toInt() + check(absentCount >= 0 && rootCount + absentCount <= cellCount) { + "Invalid BOC header: rootCount=$rootCount, absentCount=$absentCount, cellCount=$cellCount" + } + + val dataSize = source.readLong(offsetByteSize) + check(dataSize <= (cellCount.toLong() shl 10)) { + "Invalid BOC header: dataSize=$dataSize, cellCount=$cellCount" + } + check(dataSize <= (1L shl 40)) { + "Invalid BOC header: dataSize more than 1TiB? dataSize=$dataSize" + } + check(dataSize >= cellCount * (2L + refByteSize) - refByteSize) { + "Invalid BOC header: too many cells for this amount of data bytes. dataSize=$dataSize, cellCount=$cellCount, refByteSize=$refByteSize" + } + val totalSize = dataOffset + dataSize + (if (hasCrc32c) 4 else 0) + + return BagOfCellsHeader( + magic = magic, + rootCount = rootCount, + cellCount = cellCount, + absentCount = absentCount, + refByteSize = refByteSize, + offsetByteSize = offsetByteSize, + hasIndex = hasIndex, + hasRoots = hasRoots, + hasCrc32c = hasCrc32c, + hasCacheBits = hasCacheBits, + rootsOffset = rootsOffset, + indexOffset = indexOffset, + dataOffset = dataOffset, + dataSize = dataSize, + totalSize = totalSize + ) + } + } +} diff --git a/tvm/src/cell/Cell.kt b/tvm/src/cell/Cell.kt index 7c5d5754..3584063f 100644 --- a/tvm/src/cell/Cell.kt +++ b/tvm/src/cell/Cell.kt @@ -1,4 +1,4 @@ -@file:Suppress("OPT_IN_USAGE") +@file:Suppress("OPT_IN_USAGE", "PackageDirectoryMismatch", "DEPRECATION") package org.ton.cell @@ -8,17 +8,24 @@ import kotlin.jvm.JvmStatic @JsonClassDiscriminator("@type") public interface Cell { - public val bits: BitString - public val refs: List + @Deprecated("Cells don't always has loaded bits, use DataCell, which guarantees loaded bits") + public val bits: BitString get() = BitString.empty() + + @Deprecated("Cells don't always have references, use DataCell, which guarantees references") + public val refs: List get() = emptyList() + public val descriptor: CellDescriptor public val type: CellType get() = descriptor.cellType public val levelMask: LevelMask get() = descriptor.levelMask + @Deprecated("Cells don't always has loaded bits to determine is it empty, use DataCell, which guarantees loaded bits") public fun isEmpty(): Boolean = bits.isEmpty() && refs.isEmpty() public fun hash(level: Int = MAX_LEVEL): BitString + public fun depth(level: Int = MAX_LEVEL): Int + @Deprecated("Cells don't always have references to treeWalk, use DataCell, which guarantees references") public fun treeWalk(): Sequence = sequence { yieldAll(refs) refs.forEach { reference -> diff --git a/tvm/src/cell/DataCell.kt b/tvm/src/cell/DataCell.kt index b3c556f6..a129d01a 100644 --- a/tvm/src/cell/DataCell.kt +++ b/tvm/src/cell/DataCell.kt @@ -1,13 +1,38 @@ +@file:Suppress("PackageDirectoryMismatch") + package org.ton.cell +import kotlinx.io.bytestring.ByteString import org.ton.bitstring.BitString +import org.ton.kotlin.cell.LoadedCell public class DataCell( override val descriptor: CellDescriptor, - override val bits: BitString, - override val refs: List, - internal val hashes: List> -) : Cell { + public val hashes: List, + public val depths: List, + bits: BitString, + public override val references: List +) : LoadedCell { + @Deprecated("Use new constructor") + public constructor( + descriptor: CellDescriptor, + bits: BitString, + references: List, + hashes: List> + ) : this( + descriptor, + hashes.map { (b, _) -> ByteString(b) }, + hashes.map { (_, d) -> d.toShort() }, + bits, + references + ) + + @Suppress("OVERRIDE_DEPRECATION") + override val refs: List get() = references + + @Suppress("OVERRIDE_DEPRECATION") + override val bits: BitString = bits + private val hashCode: Int by lazy(LazyThreadSafetyMode.PUBLICATION) { var result = descriptor.hashCode() result = 31 * result + hashes.hashCode() @@ -16,12 +41,12 @@ public class DataCell( override fun hash(level: Int): BitString { val hashIndex = levelMask.apply(level).hashIndex - return BitString(hashes[hashIndex].first) + return BitString(hashes[hashIndex].toByteArray()) } override fun depth(level: Int): Int { val hashIndex = levelMask.apply(level).hashIndex - return hashes[hashIndex].second + return depths[hashIndex].toInt() } override fun virtualize(offset: Int): Cell { @@ -40,7 +65,7 @@ public class DataCell( if (descriptor != other.descriptor) return false if (bits != other.bits) return false - return refs == other.refs + return references == other.references } override fun hashCode(): Int = hashCode diff --git a/tvm/src/cell/ExternalCell.kt b/tvm/src/cell/ExternalCell.kt new file mode 100644 index 00000000..d7a0453b --- /dev/null +++ b/tvm/src/cell/ExternalCell.kt @@ -0,0 +1,39 @@ +@file:Suppress("PackageDirectoryMismatch") + +package org.ton.kotlin.cell + +import kotlinx.io.bytestring.ByteString +import org.ton.bitstring.BitString +import org.ton.cell.Cell +import org.ton.cell.CellDescriptor +import org.ton.cell.VirtualCell + +public data class ExternalCell( + override val descriptor: CellDescriptor, + val hashes: List, + val depths: List, + val index: Int, +) : Cell { + override fun hash(level: Int): BitString { + val hashIndex = descriptor.levelMask.apply(level).hashIndex + return BitString(hashes[hashIndex].toByteArray()) + } + + override fun depth(level: Int): Int { + val hashIndex = descriptor.levelMask.apply(level).hashIndex + return depths[hashIndex].toInt() + } + + override fun virtualize(offset: Int): Cell { + return if (levelMask.isEmpty()) { + this + } else { + VirtualCell(this, offset) + } + } + + @OptIn(ExperimentalStdlibApi::class) + override fun toString(): String { + return "ExternalCell(index=$index, l=${descriptor.levelMask})" + } +} diff --git a/tvm/src/cell/LoadedCell.kt b/tvm/src/cell/LoadedCell.kt new file mode 100644 index 00000000..0befb7d0 --- /dev/null +++ b/tvm/src/cell/LoadedCell.kt @@ -0,0 +1,9 @@ +@file:Suppress("PackageDirectoryMismatch") + +package org.ton.kotlin.cell + +import org.ton.cell.Cell + +public interface LoadedCell : Cell { + public val references: List +} diff --git a/tvm/src/cell/internal/IO.kt b/tvm/src/cell/internal/IO.kt new file mode 100644 index 00000000..af8b29cd --- /dev/null +++ b/tvm/src/cell/internal/IO.kt @@ -0,0 +1,68 @@ +@file:Suppress("PackageDirectoryMismatch") + +package org.ton.kotlin.cell.internal + +import io.ktor.utils.io.core.* +import kotlinx.io.* +import kotlinx.io.Buffer +import kotlinx.io.bytestring.ByteString + +internal fun Source.readLong(bytes: Int): Long { + var result = 0L + for (i in 0 until bytes) { + result = (result shl 8) or (readByte().toLong() and 0xFF) + } + return result +} + +internal interface RandomAccess { + var position: Long +} + +internal interface RandomAccessSink : RandomAccess, RawSink, AutoCloseable + +internal interface RandomAccessSource : RandomAccess, RawSource, AutoCloseable + +internal interface RandomAccessStorage : RandomAccessSink, RandomAccessSource, AutoCloseable + +internal class ByteArrayRandomAccessStorage( + private val byteArray: ByteArray +) : RandomAccessStorage { + override var position: Long = 0 + + override fun write(source: Buffer, byteCount: Long) { + source.readFully(byteArray, position.toInt(), byteCount.toInt()) + position += byteCount + } + + override fun flush() { + } + + override fun close() { + } + + override fun readAtMostTo(sink: Buffer, byteCount: Long): Long { + val bytesToRead = byteCount.coerceAtMost((byteArray.size - position.toInt()).toLong()) + if (bytesToRead <= 0) return 0 + sink.write(byteArray, position.toInt(), bytesToRead.toInt()) + position += bytesToRead + return bytesToRead + } +} + +internal class ByteStringRandomAccessSource( + private val byteString: ByteString +) : RandomAccessSource { + override var position: Long = 0 + + override fun readAtMostTo(sink: Buffer, byteCount: Long): Long { + val bytesToRead = byteCount.coerceAtMost((byteString.size - position)) + if (bytesToRead <= 0) return 0 + sink.write(byteString, position.toInt(), (position + bytesToRead).toInt()) + position += bytesToRead + return bytesToRead + } + + override fun close() { + } +} diff --git a/tvm/test/BagOfCellsTest.kt b/tvm/test/BagOfCellsTest.kt index ced4eddc..bc06bb09 100644 --- a/tvm/test/BagOfCellsTest.kt +++ b/tvm/test/BagOfCellsTest.kt @@ -1,42 +1,21 @@ -package org.ton.cell +@file:Suppress("PackageDirectoryMismatch") + +package org.ton.kotlin.cell -import io.ktor.util.* -import org.ton.boc.BagOfCells -import org.ton.boc.base64ToCell -import kotlin.io.encoding.Base64 import kotlin.test.Test import kotlin.test.assertEquals class BagOfCellsTest { @Test - fun `simple BoC from bytes`() { - val boc = BagOfCells(hex("b5ee9c72010102010011000118000001010000000000000045010000")) - assertEquals(1, boc.roots.size) + fun simpleBocFromBytes() { + val boc = BagOfCells("b5ee9c72010102010011000118000001010000000000000045010000".hexToByteArray()) + assertEquals(1, boc.rootCount) - assertEquals("000001010000000000000045", hex(boc.roots.first().bits.toByteArray())) - } + val loadedCell = boc.loadCell(boc.getRootCell()) - @Test - fun testTxBoc() { - val base64boc = - "te6cckECBwEAAXYAA7V5QBVtaGBzXelnC+DoDhctQO99SeiHtx97kPR6CqcKIBAAAY3pKFDwECaGi15kBA/sg+mS5AXFRbdn6kqS4yBQE7nybKbPnPtQAAGN4NmfWBZxzFkwACBgQSDoAQIDAgHgBAUAgnJQdVmYCFXS9KgXRepSXHT8e+rFn5z5+QuRNP8kfXkPQu1TOHkZ0mu5gLKdK7eCsCXnvOxGjTz0QiYLq2OyHJBWAB8ETQjmJaABwDAgjTMEEa1AAK9oAct9CC/XwHCKXyewXhMtNci1Zua3V+FLpl8rt7T586IZACUAVbWhgc13pZwvg6A4XLUDvfUnoh7cfe5D0egqnCiATmJaAAYII1oAADG9JJAMBM45ixpAAQHfBgC3WAEoAq2tDA5rvSzhfB0BwuWoHe+pPRD24+9yHo9BVOFEAwA5b6EF+vgOEUvk9gvCZaa5Fqzc1ur8KXTL5Xb2nz50Qw5J8AAGCCNaAAAxvSUKHgTOOYsmf////8AumLaq" - val boc = base64boc.base64ToCell() assertEquals( - "416f947497d143bc02e344fa292ace136e21c2f4b4f8b2dde039dc189b37a8ca", - boc.hash().toHexString().lowercase() + "000001010000000000000045", + loadedCell.bits.toByteArray().toHexString() ) } - - @Test - fun testDeserialization() { - val boc1 = Base64.decode( - "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" - ) - val cell1 = BagOfCells(boc1).first() - - val boc2 = BagOfCells(cell1).toByteArray() - - val cell2 = BagOfCells(boc2).first() - assertEquals(cell1, cell2) - } } diff --git a/tvm/test/LegacyBagOfCellsTest.kt b/tvm/test/LegacyBagOfCellsTest.kt new file mode 100644 index 00000000..4bdfd625 --- /dev/null +++ b/tvm/test/LegacyBagOfCellsTest.kt @@ -0,0 +1,42 @@ +package org.ton.cell + +import io.ktor.util.* +import org.ton.boc.BagOfCells +import org.ton.boc.base64ToCell +import kotlin.io.encoding.Base64 +import kotlin.test.Test +import kotlin.test.assertEquals + +class LegacyBagOfCellsTest { + @Test + fun `simple BoC from bytes`() { + val boc = BagOfCells(hex("b5ee9c72010102010011000118000001010000000000000045010000")) + assertEquals(1, boc.roots.size) + + assertEquals("000001010000000000000045", hex(boc.roots.first().bits.toByteArray())) + } + + @Test + fun testTxBoc() { + val base64boc = + "te6cckECBwEAAXYAA7V5QBVtaGBzXelnC+DoDhctQO99SeiHtx97kPR6CqcKIBAAAY3pKFDwECaGi15kBA/sg+mS5AXFRbdn6kqS4yBQE7nybKbPnPtQAAGN4NmfWBZxzFkwACBgQSDoAQIDAgHgBAUAgnJQdVmYCFXS9KgXRepSXHT8e+rFn5z5+QuRNP8kfXkPQu1TOHkZ0mu5gLKdK7eCsCXnvOxGjTz0QiYLq2OyHJBWAB8ETQjmJaABwDAgjTMEEa1AAK9oAct9CC/XwHCKXyewXhMtNci1Zua3V+FLpl8rt7T586IZACUAVbWhgc13pZwvg6A4XLUDvfUnoh7cfe5D0egqnCiATmJaAAYII1oAADG9JJAMBM45ixpAAQHfBgC3WAEoAq2tDA5rvSzhfB0BwuWoHe+pPRD24+9yHo9BVOFEAwA5b6EF+vgOEUvk9gvCZaa5Fqzc1ur8KXTL5Xb2nz50Qw5J8AAGCCNaAAAxvSUKHgTOOYsmf////8AumLaq" + val boc = base64boc.base64ToCell() + assertEquals( + "416f947497d143bc02e344fa292ace136e21c2f4b4f8b2dde039dc189b37a8ca", + boc.hash().toHexString().lowercase() + ) + } + + @Test + fun testDeserialization() { + val boc1 = Base64.decode( + "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" + ) + val cell1 = BagOfCells(boc1).first() + + val boc2 = BagOfCells(cell1).toByteArray() + + val cell2 = BagOfCells(boc2).first() + assertEquals(cell1, cell2) + } +}