From a462c91e39472902e5dcf7cf10bc32b36fa4d709 Mon Sep 17 00:00:00 2001 From: Nikita Duginets Date: Mon, 14 Oct 2024 10:56:26 +0300 Subject: [PATCH] implemented proof builder, fix hash map building fix level mask calculation --- .../src/main/java/org/ton/java/cell/Cell.java | 20 +- .../java/org/ton/java/cell/CellBuilder.java | 1 + .../java/org/ton/java/cell/TonHashMap.java | 203 ++++++++++++++++-- 3 files changed, 196 insertions(+), 28 deletions(-) diff --git a/cell/src/main/java/org/ton/java/cell/Cell.java b/cell/src/main/java/org/ton/java/cell/Cell.java index 53ed7de1..76006c13 100644 --- a/cell/src/main/java/org/ton/java/cell/Cell.java +++ b/cell/src/main/java/org/ton/java/cell/Cell.java @@ -568,7 +568,15 @@ static BocFlags parseBocFlags(byte data) { * @return String */ public String print(String indent) { - StringBuilder s = new StringBuilder(indent + "x{" + bits.toHex() + "}\n"); + String t = "x"; + if (this.type == CellType.MERKLE_PROOF) { + t = "p"; + } else if (this.type == CellType.MERKLE_UPDATE) { + t = "u"; + } else if (this.type == CellType.PRUNED_BRANCH) { + t = "P"; + } + StringBuilder s = new StringBuilder(indent + t + "{" + bits.toHex() + "}\n"); if (nonNull(refs) && !refs.isEmpty()) { for (Cell i : refs) { if (nonNull(i)) { @@ -580,15 +588,7 @@ public String print(String indent) { } public String print() { - StringBuilder s = new StringBuilder("x{" + bits.toHex() + "}\n"); - if (nonNull(refs) && !refs.isEmpty()) { - for (Cell i : refs) { - if (nonNull(i)) { - s.append(i.print(" ")); - } - } - } - return s.toString(); + return print(""); } /** Saves BoC to file */ diff --git a/cell/src/main/java/org/ton/java/cell/CellBuilder.java b/cell/src/main/java/org/ton/java/cell/CellBuilder.java index 76ba8a1a..5f5fc5a5 100644 --- a/cell/src/main/java/org/ton/java/cell/CellBuilder.java +++ b/cell/src/main/java/org/ton/java/cell/CellBuilder.java @@ -38,6 +38,7 @@ public static CellBuilder beginCell(int bitSize) { * Converts a builder into an ordinary cell. */ public Cell endCell() { + cell.levelMask = cell.resolveMask(); if (cell.getHashes().length == 0) { cell.calculateHashes(); } diff --git a/cell/src/main/java/org/ton/java/cell/TonHashMap.java b/cell/src/main/java/org/ton/java/cell/TonHashMap.java index 8db01ae6..9e63a398 100644 --- a/cell/src/main/java/org/ton/java/cell/TonHashMap.java +++ b/cell/src/main/java/org/ton/java/cell/TonHashMap.java @@ -152,6 +152,49 @@ PatriciaTreeNode flatten(PatriciaTreeNode node, int m) { } } + + private boolean isSame(String label) { + if (label.isEmpty() || label.length() == 1) { + return true; + } + for (int i = 1; i < label.length(); i++) { + if (label.charAt(i) != label.charAt(0)) { + return false; + } + } + return true; + } + + private int detectLabelType(String label, int keyLength) { + int type = 0; + // hml_short$0 {m:#} {n:#} len:(Unary ~n) {n <= m} s:(n * Bit) = HmLabel ~n m; + // first bit for 0, then n + 1 for Unary ~n and next n is n * Bit + // 0 + int bestLength = 1 + label.length() + 1 + label.length(); + + // hml_long$10 {m:#} n:(#<= m) s:(n * Bit) = HmLabel ~n m; + // first 1 + 1 for 10, then log2 bits for n and next n is n * Bit + // 1 + int labelLongLength = 1 + 1 + (int) Math.ceil(log2(keyLength + 1)) + label.length(); + + boolean isSame = isSame(label); + // hml_same$11 {m:#} v:Bit n:(#<= m) = HmLabel ~n m; + // 2 + int labelSameLength = 1 + 1 + 1 + (int) Math.ceil(log2(keyLength + 1)); + + if (labelLongLength < bestLength) { + type = 1; + bestLength = labelLongLength; + } + if (isSame) { + if (labelSameLength < bestLength) { + type = 2; + } + } + return type; + } + + /** * Serialize HashMap label * hml_short$0 {m:#} {n:#} len:(Unary ~n) {n <= m} s:(n * Bit) = HmLabel ~n m; @@ -163,15 +206,10 @@ PatriciaTreeNode flatten(PatriciaTreeNode node, int m) { * @param builder Cell to which label will be serialized */ void serialize_label(String label, int m, CellBuilder builder) { - int n = label.length(); - if (label.isEmpty()) { - builder.storeBit(false); //hml_short$0 - builder.storeBit(false); //Unary 0 - return; - } - + int t = detectLabelType(label, m); int sizeOfM = BigInteger.valueOf(m).bitLength(); - if (n < sizeOfM) { + if (t == 0) { + int n = label.length(); builder.storeBit(false); // hml_short for (int i = 0; i < n; i++) { builder.storeBit(true); // Unary n @@ -180,22 +218,20 @@ void serialize_label(String label, int m, CellBuilder builder) { for (Character c : label.toCharArray()) { builder.storeBit(c == '1'); } - return; - } - boolean isSame = (label.equals(Utils.repeat("0", label.length())) || label.equals(Utils.repeat("10", label.length()))); - - if (isSame) { - builder.storeBit(true); - builder.storeBit(true); //hml_same - builder.storeBit(label.charAt(0) == '1'); - builder.storeUint(label.length(), sizeOfM); - } else { + } else if (t == 1) { builder.storeBit(true); builder.storeBit(false); //hml_long builder.storeUint(label.length(), sizeOfM); for (Character c : label.toCharArray()) { builder.storeBit(c == '1'); } + } else if (t == 2) { + builder.storeBit(true); + builder.storeBit(true); //hml_same + builder.storeBit(label.charAt(0) == '1'); + builder.storeUint(label.length(), sizeOfM); + } else { + throw new IllegalStateException("Unknown label type: " + t); } } @@ -325,4 +361,135 @@ public Object getValueByIndex(long index) { } throw new Error("value not found at index " + index); } + + public Cell buildMerkleProof( + Object key, + Function keySerializer, + Function valueSerializer + ) { + Cell dictCell = CellBuilder.beginCell().storeDictInLine( + this.serialize(keySerializer, valueSerializer) + ).endCell(); + + Cell cell = generateMerkleProof( + "", + CellSlice.beginParse(dictCell), + keySize, + padString(keySerializer.apply(key).toBitString(), keySize, '0') + ); + + return convertToMerkleProof(cell); + } + + private int readUnaryLength(CellSlice slice) { + int res = 0; + while (slice.loadBit()) { + res++; + } + return res; + } + + private Cell generateMerkleProof( + String prefix, + CellSlice slice, + int n, + String key + ) { + Cell originalCell = CellBuilder.beginCell().storeSlice(slice).endCell(); + + int lb0 = slice.loadBit() ? 1 : 0; + int prefixLength; + StringBuilder pp = new StringBuilder(prefix); + + if (lb0 == 0) { + // Short label detected + prefixLength = readUnaryLength(slice); + for (int i = 0; i < prefixLength; i++) { + pp.append(slice.loadBit() ? '1' : '0'); + } + } else { + int lb1 = slice.loadBit() ? 1 : 0; + if (lb1 == 0) { + // Long label detected + prefixLength = slice.loadUint((int) Math.ceil(Math.log(n + 1) / Math.log(2))).intValue(); + for (int i = 0; i < prefixLength; i++) { + pp.append(slice.loadBit() ? '1' : '0'); + } + } else { + // Same label detected + char bit = slice.loadBit() ? '1' : '0'; + prefixLength = slice.loadUint((int) (Math.ceil(Math.log(n + 1) / Math.log(2)))).intValue(); + for (int i = 0; i < prefixLength; i++) { + pp.append(bit); + } + } + } + + if (n - prefixLength == 0) { + return originalCell; + } else { + CellSlice sl = CellSlice.beginParse(originalCell); + Cell left = sl.loadRef(); + Cell right = sl.loadRef(); + // NOTE: Left and right branches implicitly contain prefixes '0' and '1' + + if (left.getCellType() == CellType.ORDINARY) { + left = (pp.toString() + '0').equals(key.substring(0, pp.length() + 1)) + ? generateMerkleProof(pp.toString() + '0', CellSlice.beginParse(left), n - prefixLength - 1, key) + : convertToPrunedBranch(left); + } + + if (right.getCellType() == CellType.ORDINARY) { + right = (pp.toString() + '1').equals(key.substring(0, pp.length() + 1)) + ? generateMerkleProof(pp.toString() + '1', CellSlice.beginParse(right), n - prefixLength - 1, key) + : convertToPrunedBranch(right); + } + + return CellBuilder.beginCell() + .storeSlice(sl) + .storeRef(left) + .storeRef(right) + .endCell(); + } + } + + private Cell endExoticCell(CellBuilder builder, CellType type) { + Cell c = builder.endCell(); + Cell exotic = new Cell(c.getBits(), c.getBitLength(), c.getRefs(), true, type); + exotic.calculateHashes(); + return exotic; + } + + private Cell convertToMerkleProof(Cell c) { + return endExoticCell( + CellBuilder.beginCell() + .storeUint(3, 8) + .storeBytes(c.getHash(0)) + .storeUint(c.getDepthLevels()[0], 16) + .storeRef(c), + CellType.MERKLE_PROOF + ); + } + + private Cell convertToPrunedBranch(Cell c) { + return endExoticCell( + CellBuilder.beginCell() + .storeUint(1, 8) + .storeUint(1, 8) + .storeBytes(c.getHash(0)) + .storeUint(c.getDepthLevels()[0], 16), + CellType.PRUNED_BRANCH + ); + } + + private String padString(String original, int requiredSize, char padChar) { + if (original.length() >= requiredSize) { + return original; // No padding needed + } + + return String.valueOf(padChar) + .repeat(Math.max(0, requiredSize - original.length() + 1)) + + original; + } + } \ No newline at end of file