From a14a46191678a1a2cd397404dba72443cc7fa5b6 Mon Sep 17 00:00:00 2001
From: Babak
Date: Fri, 14 Jun 2024 14:54:06 -0600
Subject: [PATCH] condensed path impl checkpoint (wip)
---
skipledger-base/pathpack_format.md | 16 +-
.../main/java/io/crums/sldg/BaggedRow.java | 25 +-
.../main/java/io/crums/sldg/CondensedRow.java | 66 ++++++
.../java/io/crums/sldg/LevelsPointer.java | 172 +++++++++++++-
.../src/main/java/io/crums/sldg/Path.java | 46 +++-
.../src/main/java/io/crums/sldg/PathBag.java | 71 +++++-
.../src/main/java/io/crums/sldg/PathPack.java | 216 ++++++++++++------
.../java/io/crums/sldg/PathPackBuilder.java | 8 +-
.../src/main/java/io/crums/sldg/Row.java | 83 ++++---
.../src/main/java/io/crums/sldg/RowBag.java | 15 +-
.../main/java/io/crums/sldg/SerialRow.java | 22 +-
.../main/java/io/crums/sldg/SkipLedger.java | 21 +-
.../io/crums/sldg/json/PathPackParser.java | 2 +-
.../test/java/io/crums/sldg/PathPackTest.java | 23 ++
14 files changed, 613 insertions(+), 173 deletions(-)
create mode 100644 skipledger-base/src/main/java/io/crums/sldg/CondensedRow.java
diff --git a/skipledger-base/pathpack_format.md b/skipledger-base/pathpack_format.md
index 0f45fc4..e8f5294 100644
--- a/skipledger-base/pathpack_format.md
+++ b/skipledger-base/pathpack_format.md
@@ -32,15 +32,15 @@ The components of the structure are first defined, and the final object `PATH_PA
TYPE := BYTE // 0 means full; 1 means condensed
- RS_COUNT := INT // stitch row number count
- STITCH_RNS := LONG ^RS_COUNT // stitch row numbers in strictly ascending order
+ SR_COUNT := INT // stitch row no. count
+ STITCH_RNS := LONG ^SR_COUNT // stitch row no.s in strictly ascending order
- I_COUNT := INT // number of rows with full info (have input hash)
- // inferred from SkipLedger#stitch(..)
- // The full row no.s themselves (I_COUNT -many of them)
- // are also known at this point.
+ I_COUNT := INT // no. of rows in the path (have input hashes)
+ // The path's row no.s are inferred from
+ // SkipLedger#stitch(STITCH_RNS)
+ // I_COUNT is the size of that list
- R_COUNT := INT // number of rows with only ref-hashes inferred from
+ R_COUNT := INT // no. of rows with only ref-hashes inferred from
// depending on type, the size of:
//
// TYPE [0] (full)
@@ -71,6 +71,6 @@ The components of the structure are first defined, and the final object `PATH_PA
// In this way, fs the byte-size of the funnel
// block is determined from the STITCH_RNS.
- PATH_PACK := TYPE RS_COUNT STITCH_RNS R_TBL I_TBL [FUNNELS]
+ PATH_PACK := SR_COUNT STITCH_RNS TYPE R_TBL I_TBL [FUNNELS]
\ No newline at end of file
diff --git a/skipledger-base/src/main/java/io/crums/sldg/BaggedRow.java b/skipledger-base/src/main/java/io/crums/sldg/BaggedRow.java
index 03ef315..3c4d074 100644
--- a/skipledger-base/src/main/java/io/crums/sldg/BaggedRow.java
+++ b/skipledger-base/src/main/java/io/crums/sldg/BaggedRow.java
@@ -4,7 +4,6 @@
package io.crums.sldg;
import java.nio.ByteBuffer;
-import java.util.Objects;
/**
* Row backed by data in a {@linkplain RowBag}.
@@ -14,7 +13,9 @@
*/
public class BaggedRow extends Row {
- private final long rowNumber;
+ private final long rowNo;
+ private final LevelsPointer levelsPtr;
+
private final RowBag bag;
/**
@@ -22,27 +23,25 @@ public class BaggedRow extends Row {
* In order to create a subclass that does validate, invoke {@linkplain #hash()}
* in the constructor.
*/
- public BaggedRow(long rowNumber, RowBag bag) {
- this.rowNumber = rowNumber;
- this.bag = Objects.requireNonNull(bag, "null bag");
- SkipLedger.checkRealRowNumber(rowNumber);
+ public BaggedRow(long rowNo, RowBag bag) {
+ this.rowNo = rowNo;
+ this.levelsPtr = bag.levelsPointer(rowNo);
+ this.bag = bag;
}
@Override
public LevelsPointer levelsPointer() {
- return bag.levelsPointer(rowNumber);
+ return levelsPtr;
}
- // @Override
- // public final long no() {
- // return rowNumber;
- // }
-
@Override
public ByteBuffer inputHash() {
- return bag.inputHash(rowNumber);
+ return bag.inputHash(rowNo);
}
+
+
+
}
diff --git a/skipledger-base/src/main/java/io/crums/sldg/CondensedRow.java b/skipledger-base/src/main/java/io/crums/sldg/CondensedRow.java
new file mode 100644
index 0000000..f8445ef
--- /dev/null
+++ b/skipledger-base/src/main/java/io/crums/sldg/CondensedRow.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 Babak Farhang
+ */
+package io.crums.sldg;
+
+import static io.crums.sldg.SkipLedger.levelLinked;
+
+import java.nio.ByteBuffer;
+
+/**
+ *
+ */
+public class CondensedRow extends Row {
+
+
+ public static Row compressToLevelRowNo(Row row, long levelRn) {
+
+ int level = levelLinked(levelRn, row.no());
+ if (level < 0)
+ throw new IllegalArgumentException(
+ "levelRn " + levelRn + " not covered by row " + row);
+
+ if (row.levelsPointer().coversLevel(level))
+ return row.isCompressed() ?
+ row : new CondensedRow(row, level, true);
+
+ throw new IllegalArgumentException(
+ "levelRn " + levelRn + " not covered by row " + row.levelsPointer());
+ }
+
+
+
+
+
+ private final Row row;
+
+ private final int refLevel;
+
+
+ private CondensedRow(Row row, int level, boolean trustMe) {
+ this.row = row;
+ this.refLevel = level;
+ }
+
+ public CondensedRow(Row row, int level) {
+ this(row, level, true);
+
+ LevelsPointer levelsPtr = row.levelsPointer();
+ if (!levelsPtr.coversLevel(level))
+ throw new IllegalArgumentException(
+ "level " + level + " not covered: " + levelsPtr);
+ }
+
+
+ @Override
+ public LevelsPointer levelsPointer() {
+ return row.levelsPointer().compressToLevel(refLevel);
+ }
+
+ @Override
+ public ByteBuffer inputHash() {
+ return row.inputHash();
+ }
+
+
+}
diff --git a/skipledger-base/src/main/java/io/crums/sldg/LevelsPointer.java b/skipledger-base/src/main/java/io/crums/sldg/LevelsPointer.java
index 60ee5ac..222828a 100644
--- a/skipledger-base/src/main/java/io/crums/sldg/LevelsPointer.java
+++ b/skipledger-base/src/main/java/io/crums/sldg/LevelsPointer.java
@@ -5,9 +5,10 @@
import static java.util.Collections.binarySearch;
-
+import static io.crums.sldg.SkipLedger.alwaysAllLevels;
import static io.crums.sldg.SkipLedger.dupAndCheck;
import static io.crums.sldg.SkipLedger.skipCount;
+
import static io.crums.sldg.SldgConstants.DIGEST;
import java.nio.ByteBuffer;
@@ -21,7 +22,7 @@
* from the row's input-hash and the merklized hash of the previous
* row-hashes (the so-called row's "levels"). This class offers 2 ways for
* calculating a row's merklized levels' {@linkplain #hash() hash} (which
- * doesn't concern the input-hash):
+ * never involves the input-hash):
*
* - Using all the levels' row-hashes.
* - Using a merkle-proof consisting of a single level row-hash,
@@ -34,7 +35,7 @@
* (where {@code n} here is the difference of the highest and lowest row no. in
* the path). Using the second option this class offers (its so called
* {@linkplain #isCondensed() condensed} version), a skip path typically
- * contains {@code log(log(n)) x log(n)} many hashes.
+ * contains {@code log(log(n)) x log(n)} many 32-byte hashes.
*
*/
public class LevelsPointer {
@@ -64,7 +65,7 @@ public LevelsPointer(
this.rn = rn;
this.level = level;
this.levelHash = dupAndCheck(levelHash);
- this.hashes = funnel;
+ this.hashes = dupAndCheck(funnel);
// check args
final int levels = skipCount(rn);
@@ -84,6 +85,9 @@ public LevelsPointer(
+
+
+
/**
* Creates a full-info ("un-"{@linkplain #isCondensed() condensed}) instance.
* Note the {@code prevHashes} parameter is indexed by level, so the
@@ -92,13 +96,14 @@ public LevelsPointer(
* @param rn row no.
* @param prevHashes level row hashes of size
* {@link SkipLedger#skipCount(long)
- * SkipLedger.skipCount(rn)}
+ * SkipLedger.skipCount(rn)}; contents not copied:
+ * do not modify
*/
public LevelsPointer(long rn, List prevHashes) {
this.rn = rn;
this.level = -1;
this.levelHash = null;
- this.hashes = prevHashes;
+ this.hashes = dupAndCheck(prevHashes);
final int levels = skipCount(rn);
if (prevHashes.size() != levels)
@@ -111,13 +116,47 @@ public LevelsPointer(long rn, List prevHashes) {
throw new IllegalArgumentException(
"hash for the sentinel row[0] not zeroed -- required by the protocol");
}
-
- public final boolean isCondensed() {
- return level >= 0;
+
+
+ /**
+ * Package-access (trusted caller), full-info constructor.
+ * (Offers a way to lazy-load the hashes using a functor-list.)
+ *
+ * @param rn row no.
+ * @param prevHashes neither copied not verified;
+ * a perhaps functor list
+ * @param trustMe un-used constructor disambiguator
+ */
+ LevelsPointer(long rn, List prevHashes, boolean trustMe) {
+ this.rn = rn;
+ this.level = -1;
+ this.levelHash = null;
+ this.hashes = prevHashes;
+ }
+
+
+
+ /**
+ * Package-access (trusted caller), condensed-instance constructor.
+ * (Offers a way to lazy-load the hashes using a functor-list.)
+ *
+ * @param rn row no.
+ * @param prevHashes neither copied not verified;
+ * a perhaps functor list
+ * @param trustMe un-used constructor disambiguator
+ */
+ LevelsPointer(
+ long rn, int level, ByteBuffer levelHash, List funnel,
+ boolean trustMe) {
+ this.rn = rn;
+ this.level = level;
+ this.levelHash = levelHash;
+ this.hashes = funnel;
}
+
/**
* Returns the row no. this instance generates the merklized pointer
* {@linkplain #hash() hash} for.
@@ -127,6 +166,92 @@ public final boolean isCondensed() {
public final long rowNo() {
return rn;
}
+
+
+ /**
+ * Determines whether the instance uses a condensed hash proof.
+ * When condensed, the instance references the hash of a single
+ * previous level. (Technically, these being Merkle proofs, there
+ * are usually 2 level row hashes referenced in each proof; for now
+ * we haven't supplied a method to convert one condensed version
+ * into the other).
+ *
+ * @see #isCompressed()
+ */
+ public final boolean isCondensed() {
+ return level >= 0;
+ }
+
+
+ /**
+ * Determines whether the instance is compressed. This codifies
+ * when an uncondensed instance may be condensed. Specifically,
+ * if no more than two (non-sentinel) level row-hashes are used
+ * to calculate the level merkle root hash, there is no point
+ * in "condensing" the representation.
+ *
+ * @return {@code SkipLedger.alwaysAllLevels(rowNo()) || isCondensed()}
+ */
+ public final boolean isCompressed() {
+ return alwaysAllLevels(rn) || isCondensed();
+ }
+
+
+
+ /**
+ * Returns a compressed version of this instance referencing
+ * the given level row no.
+ *
+ * @param levelRn one of {@link #coverage()}
+ *
+ * @see #compressToLevel(int)
+ */
+ public final LevelsPointer compressToLevelRowNo(long levelRn) {
+ var coverage = coverage();
+ int index = indexOf(coverage, levelRn);
+ if (index < 0)
+ throw new IllegalArgumentException(levelRn + " not covered; " + this);
+
+ int level = skipCount(rn) - 1 - index;
+ return compressToLevel(level);
+ }
+
+
+ /**
+ * Returns a compressed version of this instance for the specified level.
+ * This method fails if the instance is already condensed
+ * at a different level.
+ *
+ * Implementation Note
+ *
+ * Condensed instances at adjacent {@code even:odd} levels, eg (0,1) or (6,7),
+ * share exactly the same hash data. We could derive one from the other,
+ * but for now, it's not a priority.
+ *
+ *
+ * @param level level index {@code 0 <=level < SkipLedger.skipCount(rowNo())}
+ * @return this instance, if the instance is already compressed;
+ * a condensed version of this instance, o.w.
+ */
+ public final LevelsPointer compressToLevel(int level) {
+
+ if (isCompressed()) {
+ if (isCondensed() && this.level != level)
+ throw new IllegalArgumentException(
+ "level " + level + " not covered; condensed at level() " +
+ this.level + "; " + this);
+ return this;
+ }
+
+ // assert isCondensable(rn); too obvious
+
+ var fun = SkipLedger.levelsMerkleFunnel(level, hashes());
+ var levelHash = hashes.get(level).asReadOnlyBuffer();
+
+ return new LevelsPointer(rn, level, levelHash, fun, true);
+ }
+
+
@@ -136,8 +261,7 @@ public final long rowNo() {
* (or claims to have) their hash. The returned list does not include
* {@link #rowNo()}.
*
- * @return a singleton no. or strictly ascending row no.s less than
- * {@link #rowNo()}
+ * @return not empty, strictly ascending row no.s, < {@link #rowNo()}
*/
public final List coverage() {
final int skipCount = skipCount(rn);
@@ -151,14 +275,31 @@ public final List coverage() {
/**
- * Determines whether the hash of the given row no. is one of the
+ * Determines whether the hash of the row at the given row no. is one of the
* instance's level [hash] pointers.
+ *
+ * @param rn the row no. tested for coverage
*/
public final boolean coversRow(long rn) {
return indexOfCoverage(rn) >= 0;
}
+ /**
+ * Determines whether the hash of row at the given no. is one of the
+ * instance's level [hash] pointers.
+ */
+ public final boolean coversLevel(int level) {
+ if (this.level == level)
+ return true;
+
+ if (level < 0 || level >= skipCount(rn))
+ throw new IllegalArgumentException("level: " + level + "; " + this);
+
+ return !isCondensed();
+ }
+
+
private int indexOfCoverage(long rn) {
return indexOf(coverage(), rn);
}
@@ -285,6 +426,13 @@ private List hashes() {
+ @Override
+ public String toString() {
+ return "[" + rn + "] --> " + coverage();
+ }
+
+
+
}
diff --git a/skipledger-base/src/main/java/io/crums/sldg/Path.java b/skipledger-base/src/main/java/io/crums/sldg/Path.java
index daa844e..eff8726 100644
--- a/skipledger-base/src/main/java/io/crums/sldg/Path.java
+++ b/skipledger-base/src/main/java/io/crums/sldg/Path.java
@@ -1,18 +1,15 @@
/*
- * Copyright 2020-2021 Babak Farhang
+ * Copyright 2020-2024 Babak Farhang
*/
package io.crums.sldg;
-import static io.crums.sldg.SkipLedger.hiPtrHash;
import static io.crums.sldg.SldgConstants.DIGEST;
import static java.util.Collections.binarySearch;
import java.nio.ByteBuffer;
import java.util.HashMap;
-// import java.util.Collections;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;
@@ -70,9 +67,7 @@ public Path(List path) throws IllegalArgumentException, HashConflictExcepti
else if (path.size() > MAX_ROWS)
throw new IllegalArgumentException("too many rows: " + path.size());
- Row[] rows = new Row[path.size()];
- rows = path.toArray(rows);
- this.rows = Lists.asReadOnlyList(rows);
+ this.rows = List.copyOf(path);
verify();
}
@@ -90,7 +85,7 @@ else if (path.size() > MAX_ROWS)
* Copy constructor.
*/
protected Path(Path copy) {
- this.rows = Objects.requireNonNull(copy, "null copy").rows;
+ this.rows = copy.rows;
}
@@ -175,6 +170,41 @@ public final boolean isSkipPath() {
}
+ public final boolean isCondensed() {
+ int index = rows.size();
+ while (index-- > 0 && !rows.get(index).isCondensed());
+ return index != -1;
+ }
+
+
+
+ public final boolean isCompressed() {
+ int index = rows.size();
+ while (index-- > 0 && !rows.get(index).isCompressed());
+ return index != -1;
+ }
+
+
+
+
+ public final Path compress() {
+ if (isCompressed())
+ return this;
+
+ Row[] crows = new Row[rows.size()];
+ crows[0] = CondensedRow.compressToLevelRowNo(first(), first().no() - 1);
+ for (int index = 1; index < crows.length; ++index)
+ crows[index] =
+ CondensedRow.compressToLevelRowNo(
+ rows.get(index),
+ rows.get(index - 1).no());
+
+ // FIXME: uncomment after adequately tested..
+ // return new Path(Lists.asReadOnlyList(crows), null);
+ return new Path(Lists.asReadOnlyList(crows));
+ }
+
+
/**
diff --git a/skipledger-base/src/main/java/io/crums/sldg/PathBag.java b/skipledger-base/src/main/java/io/crums/sldg/PathBag.java
index 517384f..db97232 100644
--- a/skipledger-base/src/main/java/io/crums/sldg/PathBag.java
+++ b/skipledger-base/src/main/java/io/crums/sldg/PathBag.java
@@ -3,7 +3,13 @@
*/
package io.crums.sldg;
+import static io.crums.sldg.SkipLedger.rowHash;
+
import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.List;
+
+import io.crums.util.Lists;
/**
* Implementation interface of {@linkplain RowBag#rowHash(long)}. This models
@@ -30,20 +36,22 @@
* don't need to be validated against each other: they collectively represent some
* ledger.
*
- * In retrospect, this observation is a bit obvious: aside from its trailed (witnessed) rows a skip
+ * In retrospect, this observation is a bit obvious: a skip
* ledger's rows can always be regenerated from scratch. A {@linkplain RowBag} implemented this
* way, is much like regenerating a subset of a skip ledger from scratch.
*
- * Refactoring Note
- *
- * This code was lifted from the {@code RecurseHashRowPack}.
- *
*/
public interface PathBag extends RowBag {
Path path();
+ /**
+ * {@inheritDoc } By default a {@code PathBag} first looks up a
+ * row's hash using {@link #refOnlyHash(long)}, and if not found
+ * invokes {@link RowBag#getRow(long) getRow(rowNo)}.{@link BaggedRow#hash() hash()}.
+ *
+ */
@Override
default ByteBuffer rowHash(long rowNumber) {
@@ -68,13 +76,62 @@ default ByteBuffer rowHash(long rowNumber) {
" - cascaded internal error msg: " + internal.getMessage(), internal);
}
}
-
+
+
+
+ @Override
+ default LevelsPointer levelsPointer(long rowNo) {
+
+ final var pathRns = getFullRowNumbers();
+ int index = Collections.binarySearch(pathRns, rowNo);
+ if (index < 0)
+ throw new IllegalArgumentException(
+ "rowNo " + rowNo + " not contained in bag; " + this);
+
+ if (isCondensed() && SkipLedger.isCondensable(rowNo)) {
+ int level;
+ long refedRowNo;
+ if (index == 0) {
+ level = 0;
+ refedRowNo = rowNo - 1;
+ } else {
+ refedRowNo = pathRns.get(index - 1);
+ long diff = rowNo - refedRowNo;
+ level = Long.numberOfTrailingZeros(diff);
+ if (diff <= 0 || diff != Long.highestOneBit(diff) ||
+ level >= SkipLedger.skipCount(rowNo))
+ throw new IllegalArgumentException(
+ "assertion failure: row [" + rowNo + "]; not linked: " + pathRns +
+ "; implemenation class: " + getClass());
+ }
+
+ var funnel = getFunnel(rowNo, level).orElseThrow(
+ () -> new IllegalArgumentException(
+ "expected funnel [" + rowNo + ":" + level + "] not found"));
+
+ return
+ new LevelsPointer(rowNo, level, rowHash(refedRowNo), funnel);
+ }
+
+
+ final int levels = SkipLedger.skipCount(rowNo);
+ List levelHashes =
+ Lists.functorList(levels, li -> rowHash(rowNo - (1L << li)));
+ return new LevelsPointer(rowNo, levelHashes);
+
+ }
/**
* Returns the ref-only row-hash from the minimal bag, if found;
* {@code null}, otherwise.
*/
- ByteBuffer refOnlyHash(long rowNumber);
+ ByteBuffer refOnlyHash(long rowNo);
+
+
+ /**
+ * Tests whether this instance contains condensed rows.
+ */
+ boolean isCondensed();
}
diff --git a/skipledger-base/src/main/java/io/crums/sldg/PathPack.java b/skipledger-base/src/main/java/io/crums/sldg/PathPack.java
index d045e13..6d113cf 100644
--- a/skipledger-base/src/main/java/io/crums/sldg/PathPack.java
+++ b/skipledger-base/src/main/java/io/crums/sldg/PathPack.java
@@ -9,11 +9,10 @@
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
-import java.util.SortedSet;
-import java.util.TreeSet;
import io.crums.io.Serial;
import io.crums.io.buffer.BufferUtils;
@@ -33,6 +32,7 @@ public class PathPack implements PathBag, Serial {
/** int size */
private final static int STITCH_COUNT_SIZE = 4;
+ private final static int TYPE_SIZE = 1;
/**
* Loads and returns a new instance by reading the given buffer.
@@ -119,10 +119,15 @@ public static PathPack forPath(Path path) {
// so the returned pack object doesn't internally
// reference the given path argument
- List inputRns = Lists.readOnlyCopy(path.rowNumbers());
-
- var refOnlyRns = SkipLedger.refOnlyCoverage(inputRns).tailSet(1L);
-
+ List inputRns = List.copyOf(path.rowNumbers());
+
+ final boolean condensed = path.isCondensed();
+
+ var refOnlyRns = ( condensed ?
+ SkipLedger.refOnlyCondensedCoverage(inputRns) :
+ SkipLedger.refOnlyCoverage(inputRns) )
+ .tailSet(1L);
+
var refHashes = ByteBuffer.allocate(
refOnlyRns.size() * SldgConstants.HASH_WIDTH);
@@ -141,8 +146,49 @@ public static PathPack forPath(Path path) {
assert !inputHashes.hasRemaining();
inputHashes.flip();
+
+ if (!condensed)
+ return new PathPack(inputRns, refHashes, inputHashes);
+
+
+
- return new PathPack(inputRns, refHashes, inputHashes);
+ var funnelList = new ArrayList>();
+
+
+ long lastRn = path.loRowNumber() - 1L;
+ int hashCount = 0;
+
+ try {
+ for (Row row : path.rows().subList(1, path.length())) {
+ var levelsPtr = row.levelsPointer().compressToLevelRowNo(lastRn);
+ lastRn = levelsPtr.rowNo();
+ if (!levelsPtr.isCondensed())
+ continue;
+ var funnel = levelsPtr.funnel();
+ hashCount += funnel.size();
+ funnelList.add(funnel);
+ }
+ } catch (IllegalArgumentException iax) {
+ if (lastRn == path.loRowNumber() - 1L)
+ throw new IllegalArgumentException(
+ "not enuf info to pack first row [" + lastRn +
+ "]: " + iax.getMessage());
+
+ throw iax;
+ }
+
+ assert hashCount > 0;
+
+ var funnels = ByteBuffer.allocate(hashCount * HASH_WIDTH);
+ for (var funnel : funnelList)
+ for (var hash : funnel)
+ funnels.put(hash);
+
+ assert !funnels.hasRemaining();
+ funnels.flip();
+
+ return new PathPack(inputRns, refHashes, inputHashes, funnels);
}
@@ -157,25 +203,17 @@ public static PathPack forPath(Path path) {
private final ByteBuffer hashes;
private final ByteBuffer inputs;
- private final boolean condensed; // redundant, but good for clarity
private final ByteBuffer funnels;
+ private final List funnelRns;
private final List funnelOffsets;
-
-// /**
-// *
-// */
-// private PathPack() {
-// hashRns = inputRns = List.of();
-// hashes = inputs = BufferUtils.NULL_BUFFER;
-// }
/**
* Creates a new "un-condensed" instance. Caller agrees not to change the contents
- * of the inputs.
+ * of the input arguments.
*
* @param inputRns ascending row no.s with input hashes (full rows)
* @param hashes ref-only hashes (row no.s inferred from {@code inputRns}
@@ -184,33 +222,85 @@ public static PathPack forPath(Path path) {
PathPack(List inputRns, ByteBuffer hashes, ByteBuffer inputs) {
this.inputRns = Objects.requireNonNull(inputRns, "null inputRns");
- SortedSet refs = SkipLedger.refOnlyCoverage(inputRns).tailSet(1L);
- Objects.requireNonNull(hashes, "null hashes");
- Objects.requireNonNull(inputs, "null inputs");
- this.hashRns = Lists.readOnlyCopy(refs);
+ this.hashRns = List.copyOf(
+ SkipLedger.refOnlyCoverage(inputRns).tailSet(1L)); // sans 0L
+
this.hashes = BufferUtils.readOnlySlice(hashes);
this.inputs = BufferUtils.readOnlySlice(inputs);
+
+ this.funnels = BufferUtils.NULL_BUFFER;
+ this.funnelRns = List.of();
+ this.funnelOffsets = List.of();
- this.condensed = false;
- this.funnels = null;
- this.funnelOffsets = null;
- // if the following throw, there's a bug..
+ checkCommonArgs(hashes, inputs);
+ }
+
+
+ private void checkCommonArgs(ByteBuffer hashes, ByteBuffer inputs) {
+
if (inputRns.isEmpty())
throw new IllegalArgumentException("inputRns is empty");
- if (hashRns.size() * HASH_WIDTH != this.hashes.remaining())
- throw new IllegalArgumentException(
- "hash row numbers / buffer size mismatch:\n" +
- hashRns + "\n " + this.hashes);
+ checkRnsBuffer(
+ inputRns, inputs, "input row no.s / buffer size mismatch");
+ checkRnsBuffer(
+ hashRns, hashes, "hash row no.s / buffer size mismatch");
+ }
- if (inputRns.size() * HASH_WIDTH != this.inputs.remaining())
- throw new IllegalArgumentException(
- "input row numbers / buffer size mismatch:\n" +
- inputRns + "\n " + this.inputs);
+
+ private void checkRnsBuffer(
+ List rns, ByteBuffer buffer, String preamble) {
+ if (rns.size() * HASH_WIDTH != buffer.remaining())
+ throw new IllegalArgumentException(
+ preamble + ":\n " + rns + "\n " + buffer);
+ }
+
+
+
+ PathPack(
+ List inputRns, ByteBuffer hashes, ByteBuffer inputs,
+ ByteBuffer funnels) {
+ this.inputRns = inputRns;
+ this.hashRns = List.copyOf(
+ SkipLedger.refOnlyCondensedCoverage(inputRns).tailSet(1L)); // sans 0L
+
+ this.hashes = BufferUtils.readOnlySlice(hashes);
+ this.inputs = BufferUtils.readOnlySlice(inputs);
+ this.funnels = BufferUtils.readOnlySlice(funnels);
+
+ checkCommonArgs(hashes, inputs);
+
+ var condensedRns = new ArrayList(inputRns.size());
+ var funOffs = new ArrayList(inputRns.size());
+
+ long prevRn = inputs.get(0) - 1L;
+ assert prevRn >= 0;
+ int offset = 0;
+
+ for (Long rn : inputRns) {
+ assert prevRn < rn;
+ if (SkipLedger.isCondensable(rn)) {
+ condensedRns.add(rn);
+ funOffs.add(offset);
+ offset += SkipLedger.funnelLength(prevRn, rn);
+ }
+ prevRn = rn;
+ }
+
+ if (condensedRns.isEmpty())
+ throw new IllegalArgumentException("no condensable rows: " + inputRns);
+
+ if (offset != this.funnels.remaining())
+ throw new IllegalArgumentException(
+ "expected " + offset + " funnel bytes; actual was " +
+ this.funnels.remaining() + "; funnels " + funnels + ": " + inputRns);
+
+ this.funnelRns = Lists.asReadOnlyList(condensedRns);
+ this.funnelOffsets = Lists.asReadOnlyList(funOffs);
}
@@ -227,12 +317,17 @@ public PathPack(PathPack copy) {
this.hashes = copy.hashes;
this.inputs = copy.inputs;
- this.condensed = copy.condensed;
this.funnels = copy.funnels;
+ this.funnelRns = copy.funnelRns;
this.funnelOffsets = copy.funnelOffsets;
}
+ @Override
+ public final boolean isCondensed() {
+ return !funnelRns.isEmpty();
+ }
+
public final ByteBuffer inputsBlock() {
return inputs.asReadOnlyBuffer();
@@ -324,54 +419,25 @@ protected final ByteBuffer emit(ByteBuffer store, int index) {
@Override
public int serialSize() {
- final int inputsCount = inputRns.size();
- final int dataBytes = inputs.capacity() + hashes.capacity();
-
- if (inputsCount <= 2)
- return STITCH_COUNT_SIZE + inputsCount*8 + dataBytes;
+ return
+ STITCH_COUNT_SIZE +
+ preStitchRowNos().size() * 8 +
+ hashes.remaining() +
+ inputs.remaining() +
+ funnels.remaining();
- final int stitchRns;
- {
- var candidate = List.of(lo(), hi());
-
- List skipRns = SkipLedger.stitch(candidate);
-
- if (skipRns.equals(inputRns))
- stitchRns = 2;
-
- else {
- var stitchSet = new TreeSet(candidate);
- for (int index = inputsCount - 1; index-- > 1; ) {
- Long rn = inputRns.get(index);
- if (Collections.binarySearch(skipRns, rn) >= 0)
- continue;
- stitchSet.add(rn);
- skipRns = SkipLedger.stitchSet(stitchSet);
- }
- stitchRns = stitchSet.size();
- }
- }
-
- return STITCH_COUNT_SIZE + stitchRns*8 + dataBytes;
}
@Override
public ByteBuffer writeTo(ByteBuffer out) throws BufferOverflowException {
- final int inputsCount = inputRns.size();
-
- // handle the corner cases first..
- if (inputsCount <= 2) {
- out.putInt(inputsCount);
- return inputsCount == 0 ? out :
- writeLongs(inputRns, out)
- .put(hashes.slice())
- .put(inputs.slice());
- }
- // calculate the stitch row no.s..
- List stitchRns = compressedRowNos();
+ // write the type
+ // out.put((byte) (isCondensed() ? 1 : 0));
+
+ // calculate the stitch row no.s..
+ List stitchRns = preStitchRowNos();
// haven't written anything yet.. write it all out
out.putInt(stitchRns.size());
@@ -385,7 +451,7 @@ public ByteBuffer writeTo(ByteBuffer out) throws BufferOverflowException {
/**
* Returns the full row numbers in pre-stitched form.
*/
- public List compressedRowNos() {
+ public List preStitchRowNos() {
return SkipLedger.stitchCompress(inputRns);
}
diff --git a/skipledger-base/src/main/java/io/crums/sldg/PathPackBuilder.java b/skipledger-base/src/main/java/io/crums/sldg/PathPackBuilder.java
index 1e9b03d..bc2d85f 100644
--- a/skipledger-base/src/main/java/io/crums/sldg/PathPackBuilder.java
+++ b/skipledger-base/src/main/java/io/crums/sldg/PathPackBuilder.java
@@ -227,6 +227,10 @@ public int addPath(Path path) {
}
+ @Override
+ public final boolean isCondensed() {
+ return false; // FIXME
+ }
public int addPack(PathPack pack) {
if (pack.isEmpty())
@@ -269,7 +273,7 @@ private int addSansLinkCheck(SerialRow safeRow) {
if (memoHash.equals(rowHash))
return 0;
// then there's a bug
- throw new AssertionError(
+ throw new IllegalArgumentException(
"memo-ed hash at [" + rn + "] conflicts with argument: " + safeRow);
}
}
@@ -298,7 +302,7 @@ private int addSansLinkCheck(SerialRow safeRow) {
if (level < levels - 1)
this.corrupted = true;
- throw new AssertionError("[" + rn + ":" + level + "]");
+ throw new IllegalArgumentException("[" + rn + ":" + level + "]");
}
}
diff --git a/skipledger-base/src/main/java/io/crums/sldg/Row.java b/skipledger-base/src/main/java/io/crums/sldg/Row.java
index 376b275..3378db7 100644
--- a/skipledger-base/src/main/java/io/crums/sldg/Row.java
+++ b/skipledger-base/src/main/java/io/crums/sldg/Row.java
@@ -6,7 +6,6 @@
import java.nio.ByteBuffer;
import java.util.List;
-import java.util.SortedSet;
import io.crums.util.Lists;
@@ -20,6 +19,19 @@
* defined in this module are not sealed, so it's conceivable a subclass may
* violate the constraint.
*
+ * Overridable / Abstract Methods
+ *
+ * All but 3 methods in this class are marked final. 2 of those 3
+ * {@link #inputHash()} and {@link #levelsPointer()} are abstract, and
+ * together they drive the return values for the other methods. The only
+ * non-final method is {@link #hash()} which generates the RHS of the
+ * hash-relation mentioned above. The reason why it's not made final is
+ * that an implementation might have memo-ized the result of the hashing.
+ *
+ *
+ * @see #inputHash()
+ * @see #levelsPointer()
+ * @see #hash()
*
* @see RowHash#prevLevels()
* @see RowHash#prevNo(int)
@@ -27,7 +39,12 @@
public abstract class Row extends RowHash {
-
+ /**
+ * {@inheritDoc}
+ *
+ * @return {@code levelsPointer().rowNo()}
+ */
+ @Override
public final long no() {
return levelsPointer().rowNo();
}
@@ -35,7 +52,7 @@ public final long no() {
/**
* Returns the user-inputed hash. This is the hash of the abstract object
- * (whatever it is, we don't know).
+ * the entered the ledger (whatever it is, we don't know).
*
* @return non-null, 32-bytes remaining
*/
@@ -43,18 +60,21 @@ public final long no() {
-
+ /**
+ * Returns the levels pointer.
+ */
public abstract LevelsPointer levelsPointer();
/**
* {@inheritDoc}
- *
- *
+ * The default implementation derives (calculates) the hash value using
+ * the {@link #inputHash()} and the {@link #levelsPointer()}.
*
* @return {@code SkipLedger.rowHash(inputHash(), levelsPointer().hash())}
*/
+ @Override
public ByteBuffer hash() {
return SkipLedger.rowHash(inputHash(), levelsPointer().hash());
}
@@ -70,20 +90,42 @@ final List levelHashes() {
- // public boolean hasAllLevels() {
- // return SkipLedger.alwaysAllLevels(no()) || hasAllPtrs();
- // }
-
-
+ /**
+ * Tests whether the instance's hash is computed using
+ * condensed level row hashes.
+ *
+ * @return {@code levelsPointer().isCondensed()}
+ */
public final boolean isCondensed() {
return
- // !SkipLedger.alwaysAllLevels(no()) &&
levelsPointer().isCondensed();
}
+ /**
+ * Determines whether all level row hashes are present.
+ * If an in an instance is not condensed, then
+ * all level row hashes are known by the instance.
+ *
+ * @return {@code !isCondensed()}
+ */
+ public final boolean hasAllLevels() {
+ return !isCondensed();
+ }
-
+
+
+ /**
+ * Tests whether the instance's hash is computed using
+ * the minimum number of previous row hashes. Note, an instance
+ * can both have {@linkplain #hasAllLevels() all levels} and
+ * still be compressed.
+ *
+ * @return {@code levelsPointer().isCompressed()}
+ */
+ public final boolean isCompressed() {
+ return levelsPointer().isCompressed();
+ }
/**
@@ -92,21 +134,6 @@ public final boolean isCondensed() {
*/
public final ByteBuffer hash(long rowNumber) {
return rowNumber == no() ? hash() : levelsPointer().rowHash(rowNumber);
- // final long rn = no();
- // final long diff = rn - rowNumber;
- // if (diff == 0)
- // return hash();
- // var levelsPtr = levelsPointer();
- // if (levelsPtr.coversRow(rowNumber))
- // return levelsPtr.
-
-
- // int referencedRows = prevLevels();
- // if (diff < 0 || Long.highestOneBit(diff) != diff || diff > (1L << (referencedRows - 1)))
- // throw new IllegalArgumentException(
- // "rowNumber " + rowNumber + " is not covered by this row " + this);
- // int ptrLevel = 63 - Long.numberOfLeadingZeros(diff);
- // return prevHash(ptrLevel);
}
diff --git a/skipledger-base/src/main/java/io/crums/sldg/RowBag.java b/skipledger-base/src/main/java/io/crums/sldg/RowBag.java
index 08ed194..769a5ad 100644
--- a/skipledger-base/src/main/java/io/crums/sldg/RowBag.java
+++ b/skipledger-base/src/main/java/io/crums/sldg/RowBag.java
@@ -39,7 +39,11 @@ public interface RowBag {
-
+ /**
+ * Returns the level pointer.
+ * @param rowNo
+ * @return
+ */
default LevelsPointer levelsPointer(long rowNo) {
final int levels = SkipLedger.skipCount(rowNo);
List levelHashes =
@@ -49,10 +53,13 @@ default LevelsPointer levelsPointer(long rowNo) {
/**
+ * Returns the hash-funnel for the given row no. and level index,
+ * if it exists.
*
- * @param rn row no. funnel outputs row's level-merkel hash
- * @param level
- * @return
+ * @param rn row no. the funnel outputs level-merkel hash for
+ * @param level level index
+ *
+ * @return by default empty
*/
default Optional> getFunnel(long rn, int level) {
return Optional.empty();
diff --git a/skipledger-base/src/main/java/io/crums/sldg/SerialRow.java b/skipledger-base/src/main/java/io/crums/sldg/SerialRow.java
index 422869d..8505d32 100644
--- a/skipledger-base/src/main/java/io/crums/sldg/SerialRow.java
+++ b/skipledger-base/src/main/java/io/crums/sldg/SerialRow.java
@@ -4,8 +4,8 @@
package io.crums.sldg;
+import static io.crums.sldg.SkipLedger.skipCount;
import java.nio.ByteBuffer;
-import java.util.Objects;
import io.crums.util.Lists;
@@ -47,14 +47,15 @@ public static ByteBuffer toDataBuffer(Row row) {
private final long rowNumber;
private final ByteBuffer data;
+ private final LevelsPointer levelsPtr;
/**
* Constructs a new instance. This does a defensive copy (since we want a runtime
- * reference to be guarantee immutability).
+ * reference to guarantee immutability).
*
- * @param rowNumber the data number
- * @param data the data's backing data (a sequence of cells representing hashes)
+ * @param rowNumber the row number
+ * @param data the data's backing data (a sequence of cells representing hashes)
*/
public SerialRow(long rowNumber, ByteBuffer data) {
this(rowNumber, ByteBuffer.allocate(data.remaining()).put(data).flip(), false);
@@ -66,6 +67,7 @@ public SerialRow(long rowNumber, ByteBuffer data) {
public SerialRow(SerialRow copy) {
this.rowNumber = copy.rowNumber;
this.data = copy.data;
+ this.levelsPtr = copy.levelsPtr;
}
/**
@@ -85,13 +87,15 @@ public SerialRow(Row copy) {
throw new IllegalArgumentException(
"expected " + expectedBytes + " bytes for rowNumber " + rowNumber +
"; actual given is " + data);
+
+ this.levelsPtr = new LevelsPointer(
+ rowNumber,
+ Lists.functorList(skipCount(rowNumber), this::prevHashImpl));
}
public final LevelsPointer levelsPointer() {
- return new LevelsPointer(
- rowNumber,
- Lists.functorList(SkipLedger.skipCount(rowNumber), this::prevHashImpl));
+ return levelsPtr;
}
@@ -101,10 +105,6 @@ public final ByteBuffer data() {
}
- // @Override
- // public final long no() {
- // return rowNumber;
- // }
@Override
public final ByteBuffer inputHash() {
diff --git a/skipledger-base/src/main/java/io/crums/sldg/SkipLedger.java b/skipledger-base/src/main/java/io/crums/sldg/SkipLedger.java
index c2ef150..828fde7 100644
--- a/skipledger-base/src/main/java/io/crums/sldg/SkipLedger.java
+++ b/skipledger-base/src/main/java/io/crums/sldg/SkipLedger.java
@@ -371,6 +371,8 @@ public static int countFunnels(List rowNos) {
/**
* Returns the row no.s that are condensable. In a condensed path,
* these are the row no.s that link to previous rows via funnels.
+ *
+ * @return immutable list
*/
public static List filterFunneled(List rowNos) {
return rowNos.stream().filter(SkipLedger::isCondensable).toList();
@@ -452,13 +454,22 @@ public static long hiPtrNo(long rn) {
- public static ByteBuffer dupAndCheck(ByteBuffer buffer) {
- var out = buffer.slice();
+ public static ByteBuffer dupAndCheck(ByteBuffer hash) {
+ var out = hash.slice();
if (out.remaining() != SldgConstants.HASH_WIDTH)
throw new IllegalArgumentException(
- "expected " + SldgConstants.HASH_WIDTH + " remaining bytes: " + buffer);
+ "expected " + SldgConstants.HASH_WIDTH + " remaining bytes: " + hash);
return out;
}
+
+
+ public static List dupAndCheck(List hashes) {
+ ByteBuffer[] array = new ByteBuffer[hashes.size()];
+ for (int index = 0; index < array.length; ++index)
+ array[index] = dupAndCheck(hashes.get(index));
+
+ return Lists.asReadOnlyList(array);
+ }
/**
@@ -812,7 +823,7 @@ public static List stitch(List rowNumbers) {
* list. It can then be uncompressed via the {@linkplain #stitch(List)}
* method.
*
- * @param pathRns path row no.s
+ * @param pathRns path row no.s (positive and stictly ascending order)
* @return a subset of the {@code pathRns}
*
*/
@@ -822,12 +833,14 @@ public static List stitchCompress(List pathRns) {
return pathRns;
var candidate = List.of(pathRns.get(0), pathRns.get(pSize - 1));
+
List skipRns = SkipLedger.stitch(candidate);
if (skipRns.equals(pathRns))
return candidate;
var stitchList = new ArrayList(candidate);
// invariants: stitchList.get(0) == lo(); stitchList.last() == hi()
+
for (int index = 1; index < pSize - 1; ++index) {
Long rn = pathRns.get(index);
if (Collections.binarySearch(skipRns, rn) >= 0)
diff --git a/skipledger-base/src/main/java/io/crums/sldg/json/PathPackParser.java b/skipledger-base/src/main/java/io/crums/sldg/json/PathPackParser.java
index c1f8f54..8559a5b 100644
--- a/skipledger-base/src/main/java/io/crums/sldg/json/PathPackParser.java
+++ b/skipledger-base/src/main/java/io/crums/sldg/json/PathPackParser.java
@@ -49,7 +49,7 @@ public PathPackParser(HashEncoding hashCodec, String targetNosTag, String hashes
@Override
public JSONObject injectEntity(PathPack pack, JSONObject jObj) {
- var targetRns = pack.compressedRowNos();
+ var targetRns = pack.preStitchRowNos();
var jTargetNos = new JSONArray(targetRns.size());
jTargetNos.addAll(targetRns);
jObj.put(targetNosTag, jTargetNos);
diff --git a/skipledger-base/src/test/java/io/crums/sldg/PathPackTest.java b/skipledger-base/src/test/java/io/crums/sldg/PathPackTest.java
index 6b08e81..7d91ebb 100644
--- a/skipledger-base/src/test/java/io/crums/sldg/PathPackTest.java
+++ b/skipledger-base/src/test/java/io/crums/sldg/PathPackTest.java
@@ -4,8 +4,13 @@
package io.crums.sldg;
+import static io.crums.sldg.PathTest.newRandomLedger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
import java.util.List;
+import org.junit.jupiter.api.Test;
+
/**
*
*/
@@ -26,4 +31,22 @@ protected PathPack newBag(SkipLedger ledger, List rowNumbers) {
}
+
+
+ @Test
+ public void testSerialSmallest() {
+ final int size = 1;
+ SkipLedger ledger = newRandomLedger(size);
+ Path state = ledger.statePath();
+ PathPack pack = PathPack.forPath(state);
+ var bytes = pack.serialize();
+ PathPack rt = PathPack.load(bytes);
+ assertEquals(state, rt.path());
+ }
+
+
+
+
+
+
}