-
-
Notifications
You must be signed in to change notification settings - Fork 5
feat: Leios network mini-protocols (LeiosNotify/LeiosFetch) for Musashi — transport-only [ADR 0007] #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next-leios
Are you sure you want to change the base?
feat: Leios network mini-protocols (LeiosNotify/LeiosFetch) for Musashi — transport-only [ADR 0007] #167
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package com.bloxbean.cardano.yaci.core.protocol.leios; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.Objects; | ||
|
|
||
| public final class LeiosPoint { | ||
|
Check warning on line 6 in core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/leios/LeiosPoint.java
|
||
| public static final int EB_HASH_LENGTH = 32; | ||
|
|
||
| private final long slot; | ||
| private final byte[] ebHash; | ||
|
|
||
| public LeiosPoint(long slot, byte[] ebHash) { | ||
| if (slot < 0) { | ||
| throw new IllegalArgumentException("slot must be non-negative"); | ||
| } | ||
| Objects.requireNonNull(ebHash, "ebHash"); | ||
| if (ebHash.length != EB_HASH_LENGTH) { | ||
| throw new IllegalArgumentException("ebHash must be " + EB_HASH_LENGTH + " bytes"); | ||
| } | ||
|
|
||
| this.slot = slot; | ||
| this.ebHash = Arrays.copyOf(ebHash, ebHash.length); | ||
| } | ||
|
|
||
| public long getSlot() { | ||
| return slot; | ||
| } | ||
|
|
||
| public byte[] getEbHash() { | ||
| return Arrays.copyOf(ebHash, ebHash.length); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
| if (this == o) { | ||
| return true; | ||
| } | ||
| if (!(o instanceof LeiosPoint that)) { | ||
| return false; | ||
| } | ||
| return slot == that.slot && Arrays.equals(ebHash, that.ebHash); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| int result = Long.hashCode(slot); | ||
| result = 31 * result + Arrays.hashCode(ebHash); | ||
| return result; | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return "LeiosPoint{" + | ||
| "slot=" + slot + | ||
| ", ebHash=" + Arrays.toString(ebHash) + | ||
| '}'; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.bloxbean.cardano.yaci.core.protocol.leios; | ||
|
|
||
| public final class LeiosProtocolConstants { | ||
| public static final int LEIOS_NOTIFY_PROTOCOL_ID = 18; | ||
| public static final int LEIOS_FETCH_PROTOCOL_ID = 19; | ||
|
|
||
| private LeiosProtocolConstants() { | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package com.bloxbean.cardano.yaci.core.protocol.leios; | ||
|
|
||
| import com.bloxbean.cardano.yaci.core.util.CborSerializationUtil; | ||
| import com.bloxbean.cardano.yaci.core.util.HexUtil; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.Objects; | ||
|
|
||
| public final class LeiosRawCbor { | ||
|
Check warning on line 9 in core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/leios/LeiosRawCbor.java
|
||
| private final byte[] cbor; | ||
|
|
||
| public LeiosRawCbor(byte[] cbor) { | ||
| Objects.requireNonNull(cbor, "cbor"); | ||
| if (CborSerializationUtil.deserialize(cbor).length != 1) { | ||
| throw new IllegalArgumentException("raw CBOR must contain exactly one top-level value"); | ||
| } | ||
| this.cbor = Arrays.copyOf(cbor, cbor.length); | ||
| } | ||
|
|
||
| public static LeiosRawCbor of(byte[] cbor) { | ||
| return new LeiosRawCbor(cbor); | ||
| } | ||
|
|
||
| public byte[] getCbor() { | ||
| return Arrays.copyOf(cbor, cbor.length); | ||
| } | ||
|
|
||
| public String toHex() { | ||
| return HexUtil.encodeHexString(cbor); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
| if (this == o) { | ||
| return true; | ||
| } | ||
| if (!(o instanceof LeiosRawCbor that)) { | ||
| return false; | ||
| } | ||
| return Arrays.equals(cbor, that.cbor); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Arrays.hashCode(cbor); | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return "LeiosRawCbor{bytes=" + cbor.length + '}'; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| package com.bloxbean.cardano.yaci.core.protocol.leios; | ||
|
|
||
| import java.util.Collection; | ||
| import java.util.Collections; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.TreeMap; | ||
|
|
||
| public final class LeiosTxBitmap { | ||
|
Check warning on line 10 in core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/leios/LeiosTxBitmap.java
|
||
| public static final int TXS_PER_WINDOW = 64; | ||
| public static final int MAX_WINDOW_INDEX = 0xFFFF; | ||
|
|
||
| private final Map<Integer, Long> windows; | ||
|
|
||
| public LeiosTxBitmap(Map<Integer, Long> windows) { | ||
| Objects.requireNonNull(windows, "windows"); | ||
|
|
||
| TreeMap<Integer, Long> sorted = new TreeMap<>(); | ||
| windows.forEach((window, mask) -> { | ||
| validateWindow(window); | ||
| if (mask != 0L) { | ||
| sorted.put(window, mask); | ||
| } | ||
| }); | ||
|
|
||
| this.windows = Collections.unmodifiableMap(new LinkedHashMap<>(sorted)); | ||
| } | ||
|
|
||
| public static LeiosTxBitmap empty() { | ||
| return new LeiosTxBitmap(Map.of()); | ||
| } | ||
|
|
||
| public static LeiosTxBitmap firstN(int count) { | ||
| if (count < 0) { | ||
| throw new IllegalArgumentException("count must be non-negative"); | ||
| } | ||
| if (count == 0) { | ||
| return empty(); | ||
| } | ||
|
|
||
| Map<Integer, Long> windows = new LinkedHashMap<>(); | ||
| int remaining = count; | ||
| int window = 0; | ||
| while (remaining > 0) { | ||
| validateWindow(window); | ||
| int inWindow = Math.min(TXS_PER_WINDOW, remaining); | ||
| windows.put(window, firstBits(inWindow)); | ||
| remaining -= inWindow; | ||
| window++; | ||
| } | ||
|
|
||
| return new LeiosTxBitmap(windows); | ||
| } | ||
|
|
||
| public static LeiosTxBitmap fromIndices(int... indices) { | ||
| Objects.requireNonNull(indices, "indices"); | ||
| Map<Integer, Long> windows = new LinkedHashMap<>(); | ||
| for (int index : indices) { | ||
| addIndex(windows, index); | ||
| } | ||
| return new LeiosTxBitmap(windows); | ||
| } | ||
|
|
||
| public static LeiosTxBitmap fromIndices(Collection<Integer> indices) { | ||
| Objects.requireNonNull(indices, "indices"); | ||
| Map<Integer, Long> windows = new LinkedHashMap<>(); | ||
| for (Integer index : indices) { | ||
| if (index == null) { | ||
| throw new IllegalArgumentException("index cannot be null"); | ||
| } | ||
| addIndex(windows, index); | ||
| } | ||
| return new LeiosTxBitmap(windows); | ||
| } | ||
|
|
||
| public Map<Integer, Long> getWindows() { | ||
| return windows; | ||
| } | ||
|
|
||
| public boolean isEmpty() { | ||
| return windows.isEmpty(); | ||
| } | ||
|
|
||
| public Long getMask(int window) { | ||
| validateWindow(window); | ||
| return windows.get(window); | ||
| } | ||
|
|
||
| private static void addIndex(Map<Integer, Long> windows, int index) { | ||
| if (index < 0) { | ||
| throw new IllegalArgumentException("transaction index must be non-negative"); | ||
| } | ||
| int window = index / TXS_PER_WINDOW; | ||
| validateWindow(window); | ||
|
|
||
| int offset = index % TXS_PER_WINDOW; | ||
| long bit = 1L << (TXS_PER_WINDOW - 1 - offset); | ||
| windows.merge(window, bit, (left, right) -> left | right); | ||
| } | ||
|
|
||
| private static long firstBits(int count) { | ||
| if (count < 0 || count > TXS_PER_WINDOW) { | ||
| throw new IllegalArgumentException("count must be between 0 and " + TXS_PER_WINDOW); | ||
| } | ||
| if (count == 0) { | ||
| return 0L; | ||
| } | ||
| if (count == TXS_PER_WINDOW) { | ||
| return -1L; | ||
| } | ||
| return -1L << (TXS_PER_WINDOW - count); | ||
| } | ||
|
|
||
| private static void validateWindow(int window) { | ||
| if (window < 0 || window > MAX_WINDOW_INDEX) { | ||
| throw new IllegalArgumentException("window must be between 0 and " + MAX_WINDOW_INDEX); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
| if (this == o) { | ||
| return true; | ||
| } | ||
| if (!(o instanceof LeiosTxBitmap that)) { | ||
| return false; | ||
| } | ||
| return windows.equals(that.windows); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return windows.hashCode(); | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return "LeiosTxBitmap{" + | ||
| "windows=" + windows + | ||
| '}'; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "opaque bytes" wrapper eagerly full-decodes every payload it wraps.
CborSerializationUtil.deserialize(cbor)here fully parses the payload purely to assert there is exactly one top-level item — on every inbound endorser block, announcement, vote and tx-list.That contradicts the stated design ("payloads are carried as opaque CBOR, block-level deserialization deferred"): we pay a full decode of a multi-MB EB body to check a cardinality invariant, and malformed or oversized inbound CBOR aborts message construction inside what is supposed to be a dumb byte holder.
A boundary scan (single-item / end-offset check) is enough for the invariant and is O(structure) without materializing the tree —
CborByteScannerin #168 does exactly this. Alternatively, make the check opt-in for theof(...)factory used on outbound data only.