port of URKit

This commit is contained in:
Craig Raw
2020-08-02 17:54:51 +02:00
parent d50ed46caf
commit 2f8d40198e
15 changed files with 1719 additions and 5 deletions
@@ -0,0 +1,150 @@
package com.sparrowwallet.sparrow.ur;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.zip.CRC32;
public class Bytewords {
public static final String BYTEWORDS = "ableacidalsoapexaquaarchatomauntawayaxisbackbaldbarnbeltbetabiasbluebodybragbrewbulbbuzzcalmcashcatschefcityclawcodecolacookcostcruxcurlcuspcyandarkdatadaysdelidicedietdoordowndrawdropdrumdulldutyeacheasyechoedgeepicevenexamexiteyesfactfairfernfigsfilmfishfizzflapflewfluxfoxyfreefrogfuelfundgalagamegeargemsgiftgirlglowgoodgraygrimgurugushgyrohalfhanghardhawkheathelphighhillholyhopehornhutsicedideaidleinchinkyintoirisironitemjadejazzjoinjoltjowljudojugsjumpjunkjurykeepkenokeptkeyskickkilnkingkitekiwiknoblamblavalazyleaflegsliarlistlimplionlogoloudloveluaulucklungmainmanymathmazememomenumeowmildmintmissmonknailnavyneednewsnextnoonnotenumbobeyoboeomitonyxopenovalowlspaidpartpeckplaypluspoempoolposepuffpumapurrquadquizraceramprealredorichroadrockroofrubyruinrunsrustsafesagascarsetssilkskewslotsoapsolosongstubsurfswantacotasktaxitenttiedtimetinytoiltombtoystriptunatwinuglyundouniturgeuservastveryvetovialvibeviewvisavoidvowswallwandwarmwaspwavewaxywebswhatwhenwhizwolfworkyankyawnyellyogayurtzapszestzinczonezoomzero";
private static final List<String> bytewordsList;
private static final List<String> minimalBytewordsList;
static {
bytewordsList = getBytewords();
minimalBytewordsList = getMinimalBytewords();
}
public enum Style {
STANDARD, URI, MINIMAL
}
public static int getEncodedLength(int length, Style style) {
if(style == Style.STANDARD || style == Style.URI) {
return length * 4 + (length - 1);
}
return length * 2;
}
public static String encode(byte[] data, Style style) {
if(style == Style.STANDARD) {
return encode(data, " ");
}
if(style == Style.URI) {
return encode(data, "-");
}
return encodeMinimal(data);
}
public static byte[] decode(String encoded, Style style) {
if(style == Style.STANDARD) {
return decode(encoded, " ");
}
if(style == Style.URI) {
return decode(encoded, "-");
}
return decodeMinimal(encoded);
}
private static String encode(byte[] data, String separator) {
byte[] dataAndChecksum = appendChecksum(data);
List<String> words = IntStream.range(0, dataAndChecksum.length).map(index -> dataAndChecksum[index] & 0xFF).mapToObj(Bytewords::getByteword).collect(Collectors.toList());
StringJoiner joiner = new StringJoiner(separator);
words.forEach(joiner::add);
return joiner.toString();
}
private static String encodeMinimal(byte[] data) {
byte[] dataAndChecksum = appendChecksum(data);
List<String> words = IntStream.range(0, dataAndChecksum.length).map(index -> dataAndChecksum[index] & 0xFF).mapToObj(Bytewords::getMinimalByteword).collect(Collectors.toList());
StringBuilder buffer = new StringBuilder();
words.forEach(buffer::append);
return buffer.toString();
}
private static byte[] decode(String encoded, String separator) {
String[] words = encoded.split(separator);
byte[] data = toByteArray(Arrays.stream(words).mapToInt(word -> getBytewords().indexOf(word)));
return stripChecksum(data);
}
private static byte[] decodeMinimal(String encoded) {
List<String> words = splitStringBySize(encoded, 2);
byte[] data = toByteArray(words.stream().mapToInt(word -> getMinimalBytewords().indexOf(word)));
return stripChecksum(data);
}
private static byte[] appendChecksum(byte[] data) {
CRC32 crc = new CRC32();
crc.update(data);
ByteBuffer checksum = ByteBuffer.allocate(Long.BYTES);
checksum.putLong(crc.getValue());
byte[] result = new byte[data.length + 4];
System.arraycopy(data, 0, result, 0, data.length);
System.arraycopy(checksum.array(), 4, result, data.length, 4);
return result;
}
private static byte[] stripChecksum(byte[] dataAndChecksum) {
byte[] data = Arrays.copyOfRange(dataAndChecksum, 0, dataAndChecksum.length - 4);
byte[] checksum = Arrays.copyOfRange(dataAndChecksum, dataAndChecksum.length - 4, dataAndChecksum.length);
CRC32 crc = new CRC32();
crc.update(data);
ByteBuffer calculedChecksum = ByteBuffer.allocate(Long.BYTES);
calculedChecksum.putLong(crc.getValue());
if(!Arrays.equals(Arrays.copyOfRange(calculedChecksum.array(), 4, 8), checksum)) {
throw new InvalidChecksumException("Invalid checksum");
}
return data;
}
private static String getByteword(int dataByte) {
return bytewordsList.get(dataByte);
}
private static String getMinimalByteword(int dataByte) {
return minimalBytewordsList.get(dataByte);
}
private static List<String> getBytewords() {
return IntStream.range(0, 256).mapToObj(i -> BYTEWORDS.substring(i * 4, (i * 4) + 4)).collect(Collectors.toList());
}
private static List<String> getMinimalBytewords() {
return IntStream.range(0, 256).mapToObj(i -> Character.toString(BYTEWORDS.charAt(i * 4)) + BYTEWORDS.charAt((i * 4) + 3)).collect(Collectors.toList());
}
public static byte[] toByteArray(IntStream stream) {
return stream.collect(ByteArrayOutputStream::new, (baos, i) -> baos.write((byte) i),
(baos1, baos2) -> baos1.write(baos2.toByteArray(), 0, baos2.size()))
.toByteArray();
}
private static List<String> splitStringBySize(String str, int size) {
List<String> split = new ArrayList<>();
for(int i = 0; i < str.length() / size; i++) {
split.add(str.substring(i * size, Math.min((i + 1) * size, str.length())));
}
return split;
}
public static class InvalidChecksumException extends RuntimeException {
public InvalidChecksumException(String message) {
super(message);
}
}
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.ur;
public enum ResultType {
SUCCESS, FAILURE;
}
@@ -0,0 +1,92 @@
package com.sparrowwallet.sparrow.ur;
import java.util.Arrays;
import java.util.Objects;
public class UR {
private final String type;
private final byte[] data;
public UR(String type, byte[] data) throws InvalidTypeException {
if(!isURType(type)) {
throw new InvalidTypeException("Invalid UR type: " + type);
}
this.type = type;
this.data = data;
}
public String getType() {
return type;
}
public byte[] getCbor() {
return data;
}
public static boolean isURType(String type) {
for(char c : type.toCharArray()) {
if('a' <= c && c <= 'z') {
return true;
}
if('0' <= c && c <= '9') {
return true;
}
if(c == '-') {
return true;
}
}
return false;
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
UR ur = (UR) o;
return type.equals(ur.type) &&
Arrays.equals(data, ur.data);
}
@Override
public int hashCode() {
int result = Objects.hash(type);
result = 31 * result + Arrays.hashCode(data);
return result;
}
public static class URException extends Exception {
public URException(String message) {
super(message);
}
}
public static class InvalidTypeException extends URException {
public InvalidTypeException(String message) {
super(message);
}
}
public static class InvalidSchemeException extends URException {
public InvalidSchemeException(String message) {
super(message);
}
}
public static class InvalidPathLengthException extends URException {
public InvalidPathLengthException(String message) {
super(message);
}
}
public static class InvalidSequenceComponentException extends URException {
public InvalidSequenceComponentException(String message) {
super(message);
}
}
}
@@ -0,0 +1,188 @@
package com.sparrowwallet.sparrow.ur;
import co.nstant.in.cbor.CborException;
import com.sparrowwallet.sparrow.ur.fountain.FountainDecoder;
import com.sparrowwallet.sparrow.ur.fountain.FountainEncoder;
import java.util.Arrays;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class URDecoder {
private static final Pattern SEQUENCE_COMPONENT_PATTERN = Pattern.compile("(\\d+)-(\\d+)");
private final FountainDecoder fountainDecoder;
private String expectedType;
private Result result;
public URDecoder() {
this.fountainDecoder = new FountainDecoder();
}
public int getExpectedPartCount() {
return fountainDecoder.getExpectedPartCount();
}
public Set<Integer> getReceivedPartIndexes() {
return fountainDecoder.getRecievedPartIndexes();
}
public Set<Integer> getLastPartIndexes() {
return fountainDecoder.getLastPartIndexes();
}
public int getProcessedPartsCount() {
return fountainDecoder.getProcessedPartsCount();
}
public double getEstimatedPercentComplete() {
return fountainDecoder.getEstimatedPercentComplete();
}
public Result getResult() {
return result;
}
public static UR decode(String string) throws UR.URException {
ParsedURString parsedURString = parse(string);
if(parsedURString.components.length < 1) {
throw new UR.InvalidPathLengthException("Invalid path length");
}
String body = parsedURString.components[0];
return decode(parsedURString.type, body);
}
public static UR decode(String type, String body) throws UR.InvalidTypeException {
byte[] cbor = Bytewords.decode(body, Bytewords.Style.MINIMAL);
return new UR(type, cbor);
}
public boolean receivePart(String string) {
try {
// Don't process the part if we're already done
if(getResult() != null) {
return false;
}
// Don't continue if this part doesn't validate
ParsedURString parsedURString = parse(string);
if(!validatePart(parsedURString.type)) {
return false;
}
// If this is a single-part UR then we're done
if(parsedURString.components.length == 1) {
String body = parsedURString.components[0];
result = new Result(ResultType.SUCCESS, decode(parsedURString.type, body), null);
return true;
}
// Multi-part URs must have two path components: seq/fragment
if(parsedURString.components.length != 2) {
throw new UR.InvalidPathLengthException("Invalid path length");
}
String seq = parsedURString.components[0];
String fragment = parsedURString.components[1];
// Parse the sequence component and the fragment, and
// make sure they agree.
Matcher matcher = SEQUENCE_COMPONENT_PATTERN.matcher(seq);
if(matcher.matches()) {
int seqNum = Integer.parseInt(matcher.group(1));
int seqLen = Integer.parseInt(matcher.group(2));
byte[] cbor = Bytewords.decode(fragment, Bytewords.Style.MINIMAL);
FountainEncoder.Part part = FountainEncoder.Part.fromCborBytes(cbor);
if(seqNum != part.getSeqNum() || seqLen != part.getSeqLen()) {
return false;
}
if(!fountainDecoder.receivePart(part)) {
return false;
}
if(fountainDecoder.getResult() == null) {
//Not done yet
} else if(fountainDecoder.getResult().type == ResultType.SUCCESS) {
result = new Result(ResultType.SUCCESS, new UR(parsedURString.type, fountainDecoder.getResult().data), null);
} else if(fountainDecoder.getResult().type == ResultType.FAILURE) {
result = new Result(ResultType.FAILURE, null, fountainDecoder.getResult().error);
}
return true;
} else {
throw new UR.InvalidSequenceComponentException("Invalid sequence " + seq);
}
} catch(UR.URException | CborException e) {
return false;
}
}
private boolean validatePart(String type) {
if(expectedType == null) {
if(!UR.isURType(type)) {
return false;
}
expectedType = type;
} else {
return expectedType.equals(type);
}
return true;
}
static ParsedURString parse(String string) throws UR.URException {
// Don't consider case
String lowercased = string.toLowerCase();
// Validate URI scheme
if(!lowercased.startsWith("ur:")) {
throw new UR.InvalidSchemeException("Invalid scheme");
}
String path = lowercased.substring(3);
// Split the remainder into path components
String[] components = path.split("/");
// Make sure there are at least two path components
if(components.length <= 1) {
throw new UR.InvalidPathLengthException("Invalid path length");
}
// Validate the type
String type = components[0];
if(!UR.isURType(type)) {
throw new UR.InvalidTypeException("Invalid type: " + type);
}
return new ParsedURString(type, Arrays.copyOfRange(components, 1, components.length));
}
private static class ParsedURString {
public final String type;
public final String[] components;
public ParsedURString(String type, String[] components) {
this.type = type;
this.components = components;
}
}
public static class Result {
public final ResultType type;
public final UR ur;
public final String error;
public Result(ResultType type, UR ur, String error) {
this.type = type;
this.ur = ur;
this.error = error;
}
}
}
@@ -0,0 +1,69 @@
package com.sparrowwallet.sparrow.ur;
import com.sparrowwallet.sparrow.ur.fountain.FountainEncoder;
import java.util.Arrays;
import java.util.List;
import java.util.StringJoiner;
public class UREncoder {
private final UR ur;
private final FountainEncoder fountainEncoder;
public UREncoder(UR ur, int maxFragmentLen, int minFragmentLen, long firstSeqNum) {
this.ur = ur;
this.fountainEncoder = new FountainEncoder(ur.getCbor(), maxFragmentLen, minFragmentLen, firstSeqNum);
}
public boolean isComplete() {
return fountainEncoder.isComplete();
}
public boolean isSinglePart() {
return fountainEncoder.isSinglePart();
}
public String nextPart() {
FountainEncoder.Part part = fountainEncoder.nextPart();
if(isSinglePart()) {
return encode(ur);
} else {
return encodePart(ur.getType(), part);
}
}
public long getSeqNum() {
return fountainEncoder.getSeqNum();
}
public int getSeqLen() {
return fountainEncoder.getSeqLen();
}
public List<Integer> getPartIndexes() {
return fountainEncoder.getPartIndexes();
}
public static String encode(UR ur) {
String encoded = Bytewords.encode(ur.getCbor(), Bytewords.Style.MINIMAL);
return encodeUR(ur.getType(), encoded);
}
private static String encodeUR(String... pathComponents) {
return encodeURI("ur", pathComponents);
}
private static String encodeURI(String scheme, String... pathComponents) {
StringJoiner joiner = new StringJoiner("/");
Arrays.stream(pathComponents).forEach(joiner::add);
String path = joiner.toString();
return scheme + ":" + path;
}
private static String encodePart(String type, FountainEncoder.Part part) {
String seq = part.getSeqNum() + "-" + part.getSeqLen();
String body = Bytewords.encode(part.toCborBytes(), Bytewords.Style.MINIMAL);
return encodeUR(type, seq, body);
}
}
@@ -0,0 +1,149 @@
package com.sparrowwallet.sparrow.ur.fountain;
/******************************************************************************
* File: AliasMethod.java
* Author: Keith Schwarz (htiek@cs.stanford.edu)
*
* An implementation of the alias method implemented using Vose's algorithm.
* The alias method allows for efficient sampling of random values from a
* discrete probability distribution (i.e. rolling a loaded die) in O(1) time
* each after O(n) preprocessing time.
*
* For a complete writeup on the alias method, including the intuition and
* important proofs, please see the article "Darts, Dice, and Coins: Smpling
* from a Discrete Distribution" at
*
* http://www.keithschwarz.com/darts-dice-coins/
*/
import java.util.*;
public final class AliasMethod {
/* The random number generator used to sample from the distribution. */
private final Random random;
/* The probability and alias tables. */
private final int[] alias;
private final double[] probability;
/**
* Constructs a new AliasMethod to sample from a discrete distribution and
* hand back outcomes based on the probability distribution.
* <p>
* Given as input a list of probabilities corresponding to outcomes 0, 1,
* ..., n - 1, this constructor creates the probability and alias tables
* needed to efficiently sample from this distribution.
*
* @param probabilities The list of probabilities.
*/
public AliasMethod(List<Double> probabilities) {
this(probabilities, new Random());
}
/**
* Constructs a new AliasMethod to sample from a discrete distribution and
* hand back outcomes based on the probability distribution.
* <p>
* Given as input a list of probabilities corresponding to outcomes 0, 1,
* ..., n - 1, along with the random number generator that should be used
* as the underlying generator, this constructor creates the probability
* and alias tables needed to efficiently sample from this distribution.
*
* @param probabilities The list of probabilities.
* @param random The random number generator
*/
public AliasMethod(List<Double> probabilities, Random random) {
/* Begin by doing basic structural checks on the inputs. */
if (probabilities == null || random == null)
throw new NullPointerException();
if (probabilities.size() == 0)
throw new IllegalArgumentException("Probability vector must be nonempty.");
/* Allocate space for the probability and alias tables. */
probability = new double[probabilities.size()];
alias = new int[probabilities.size()];
/* Store the underlying generator. */
this.random = random;
/* Compute the average probability and cache it for later use. */
final double average = 1.0 / probabilities.size();
/* Make a copy of the probabilities list, since we will be making
* changes to it.
*/
probabilities = new ArrayList<Double>(probabilities);
/* Create two stacks to act as worklists as we populate the tables. */
Deque<Integer> small = new ArrayDeque<Integer>();
Deque<Integer> large = new ArrayDeque<Integer>();
/* Populate the stacks with the input probabilities. */
for (int i = 0; i < probabilities.size(); ++i) {
/* If the probability is below the average probability, then we add
* it to the small list; otherwise we add it to the large list.
*/
if (probabilities.get(i) >= average)
large.add(i);
else
small.add(i);
}
/* As a note: in the mathematical specification of the algorithm, we
* will always exhaust the small list before the big list. However,
* due to floating point inaccuracies, this is not necessarily true.
* Consequently, this inner loop (which tries to pair small and large
* elements) will have to check that both lists aren't empty.
*/
while (!small.isEmpty() && !large.isEmpty()) {
/* Get the index of the small and the large probabilities. */
int less = small.removeLast();
int more = large.removeLast();
/* These probabilities have not yet been scaled up to be such that
* 1/n is given weight 1.0. We do this here instead.
*/
probability[less] = probabilities.get(less) * probabilities.size();
alias[less] = more;
/* Decrease the probability of the larger one by the appropriate
* amount.
*/
probabilities.set(more,
(probabilities.get(more) + probabilities.get(less)) - average);
/* If the new probability is less than the average, add it into the
* small list; otherwise add it to the large list.
*/
if (probabilities.get(more) >= 1.0 / probabilities.size())
large.add(more);
else
small.add(more);
}
/* At this point, everything is in one list, which means that the
* remaining probabilities should all be 1/n. Based on this, set them
* appropriately. Due to numerical issues, we can't be sure which
* stack will hold the entries, so we empty both.
*/
while (!small.isEmpty())
probability[small.removeLast()] = 1.0;
while (!large.isEmpty())
probability[large.removeLast()] = 1.0;
}
/**
* Samples a value from the underlying distribution.
*
* @return A random value sampled from the underlying distribution.
*/
public int next() {
/* Generate a fair die roll to determine which column to inspect. */
int column = random.nextInt(probability.length);
/* Generate a biased coin toss to determine which option to pick. */
boolean coinToss = random.nextDouble() < probability[column];
/* Based on the outcome, return either the column or its alias. */
return coinToss? column : alias[column];
}
}
@@ -0,0 +1,274 @@
package com.sparrowwallet.sparrow.ur.fountain;
import com.sparrowwallet.sparrow.ur.ResultType;
import java.io.ByteArrayOutputStream;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.zip.CRC32;
import static com.sparrowwallet.sparrow.ur.fountain.FountainUtils.chooseFragments;
public class FountainDecoder {
private final Set<Integer> recievedPartIndexes = new TreeSet<>();
private Set<Integer> lastPartIndexes;
private int processedPartsCount = 0;
private Result result;
private long checksum;
private Set<Integer> expectedPartIndexes;
private int expectedFragmentLen;
private int expectedMessageLen;
private long expectedChecksum;
private final Map<List<Integer>, Part> simpleParts = new HashMap<>();
private Map<List<Integer>, Part> mixedParts = new HashMap<>();
private final List<Part> queuedParts = new ArrayList<>();
public int getExpectedPartCount() {
return expectedPartIndexes.size();
}
public Set<Integer> getRecievedPartIndexes() {
return recievedPartIndexes;
}
public Set<Integer> getLastPartIndexes() {
return lastPartIndexes;
}
public int getProcessedPartsCount() {
return processedPartsCount;
}
public double getEstimatedPercentComplete() {
double estimatedInputParts = (double)getExpectedPartCount() * 1.75;
return Math.min(0.99, (double)processedPartsCount / estimatedInputParts);
}
public Result getResult() {
return result;
}
private static class Part {
private final List<Integer> partIndexes;
private final byte[] data;
private int getIndex() {
return partIndexes.get(0);
}
Part(FountainEncoder.Part part) {
this.partIndexes = chooseFragments(part.getSeqNum(), part.getSeqLen(), part.getChecksum());
this.data = part.getData();
}
Part(List<Integer> indexes, byte[] data) {
this.partIndexes = indexes;
this.data = data;
}
public boolean isSimple() {
return partIndexes.size() == 1;
}
}
public static class Result {
public final ResultType type;
public final byte[] data;
public final String error;
public Result(ResultType type, byte[] data, String error) {
this.type = type;
this.data = data;
this.error = error;
}
}
public boolean receivePart(FountainEncoder.Part encoderPart) {
// Don't process the part if we're already done
if(result != null) {
return false;
}
// Don't continue if this part doesn't validate
if(!validatePart(encoderPart)) {
return false;
}
// Add this part to the queue
Part part = new Part(encoderPart);
lastPartIndexes = new HashSet<>(part.partIndexes);
enqueue(part);
// Process the queue until we're done or the queue is empty
while(result == null && !queuedParts.isEmpty()) {
processQueueItem();
}
// Keep track of how many parts we've processed
processedPartsCount += 1;
//printPartEnd();
return true;
}
private void enqueue(Part part) {
queuedParts.add(part);
}
private void printPartEnd() {
int percent = (int)Math.round(getEstimatedPercentComplete() * 100);
System.out.println("processed: " + processedPartsCount + " expected: " + getExpectedPartCount() + " received: " + recievedPartIndexes.size() + " percent: " + percent + "%");
}
private void printPart(Part part) {
List<Integer> sorted = part.partIndexes.stream().sorted().collect(Collectors.toList());
System.out.println("part indexes: " + sorted);
}
private void printState() {
List<Integer> sortedReceived = recievedPartIndexes.stream().sorted().collect(Collectors.toList());
List<List<Integer>> mixed = mixedParts.keySet().stream().map(list -> {
list.sort(Comparator.naturalOrder());
return list;
}).collect(Collectors.toList());
System.out.println("parts: " + getExpectedPartCount() + ", received: " + sortedReceived + ", mixed: " + mixed + ", queued: " + queuedParts.size() + ", result: " + result);
}
private void processQueueItem() {
Part part = queuedParts.remove(0);
//printPart(part);
if(part.isSimple()) {
processSimplePart(part);
} else {
processMixedPart(part);
}
//printState();
}
private void reduceMixed(Part by) {
// Reduce all the current mixed parts by the given part
List<Part> reducedParts = mixedParts.values().stream().map(part -> reducePart(part, by)).collect(Collectors.toList());
// Collect all the remaining mixed parts
Map<List<Integer>, Part> newMixed = new HashMap<>();
reducedParts.forEach(reducedPart -> {
// If this reduced part is now simple
if(reducedPart.isSimple()) {
// Add it to the queue
enqueue(reducedPart);
} else {
// Otherwise, add it to the list of current mixed parts
newMixed.put(reducedPart.partIndexes, reducedPart);
}
});
mixedParts = newMixed;
}
// Reduce part `a` by part `b`
private Part reducePart(Part a, Part b) {
// If the fragments mixed into `b` are a strict (proper) subset of those in `a`...
if(a.partIndexes.containsAll(b.partIndexes)) {
// The new fragments in the revised part are `a` - `b`.
List<Integer> newIndexes = new ArrayList<>(a.partIndexes);
newIndexes.removeAll(b.partIndexes);
// The new data in the revised part are `a` XOR `b`
byte[] newdata = FountainEncoder.xor(a.data, b.data);
return new Part(newIndexes, newdata);
} else {
// `a` is not reducable by `b`, so return a
return a;
}
}
private void processSimplePart(Part part) {
// Don't process duplicate parts
Integer fragmentIndex = part.partIndexes.get(0);
if(recievedPartIndexes.contains(fragmentIndex)) {
return;
}
// Record this part
simpleParts.put(part.partIndexes, part);
recievedPartIndexes.add(fragmentIndex);
// If we've received all the parts
if(recievedPartIndexes.equals(expectedPartIndexes)) {
// Reassemble the message from its fragments
List<Part> sortedParts = simpleParts.values().stream().sorted(Comparator.comparingInt(Part::getIndex)).collect(Collectors.toList());
List<byte[]> fragments = sortedParts.stream().map(part1 -> part1.data).collect(Collectors.toList());
byte[] message = joinFragments(fragments, expectedMessageLen);
// Verify the message checksum and note success or failure
CRC32 crc32 = new CRC32();
crc32.update(message);
checksum = crc32.getValue();
if(checksum == expectedChecksum) {
result = new Result(ResultType.SUCCESS, message, null);
} else {
result = new Result(ResultType.FAILURE, null, "Invalid checksum");
}
} else {
// Reduce all the mixed parts by this part
reduceMixed(part);
}
}
private void processMixedPart(Part part) {
// Don't process duplicate parts
if(mixedParts.containsKey(part.partIndexes)) {
return;
}
// Reduce this part by all the others
List<Part> allParts = new ArrayList<>(simpleParts.values());
allParts.addAll(mixedParts.values());
Part p = allParts.stream().reduce(part, this::reducePart);
// If the part is now simple
if(p.isSimple()) {
// Add it to the queue
enqueue(p);
} else {
// Reduce all the mixed parts by this one
reduceMixed(p);
// Record this new mixed part
mixedParts.put(p.partIndexes, p);
}
}
private boolean validatePart(FountainEncoder.Part part) {
// If this is the first part we've seen
if(expectedPartIndexes == null) {
// Record the things that all the other parts we see will have to match to be valid.
expectedPartIndexes = IntStream.range(0, part.getSeqLen()).boxed().collect(Collectors.toSet());
expectedMessageLen = part.getMessageLen();
expectedChecksum = part.getChecksum();
expectedFragmentLen = part.getData().length;
return true;
} else {
return getExpectedPartCount() == part.getSeqLen() && expectedMessageLen == part.getMessageLen() && expectedChecksum == part.getChecksum() && expectedFragmentLen == part.getData().length;
}
}
static byte[] joinFragments(List<byte[]> fragments, int messageLen) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
fragments.forEach(baos::writeBytes);
byte[] message = baos.toByteArray();
byte[] unpaddedMessage = new byte[messageLen];
System.arraycopy(message, 0, unpaddedMessage, 0, messageLen);
return unpaddedMessage;
}
}
@@ -0,0 +1,182 @@
package com.sparrowwallet.sparrow.ur.fountain;
import co.nstant.in.cbor.CborBuilder;
import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.CborEncoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.Array;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.UnsignedInteger;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.CRC32;
import static com.sparrowwallet.sparrow.ur.fountain.FountainUtils.chooseFragments;
public class FountainEncoder {
private final int messageLen;
private final long checksum;
private final int fragmentLen;
private final List<byte[]> fragments;
private final int seqLen;
private List<Integer> partIndexes;
private long seqNum;
public FountainEncoder(byte[] message, int maxFragmentLen, int minFragmentLen, long firstSeqNum) {
if(message.length >= Integer.MAX_VALUE) {
throw new IllegalArgumentException("Message too long");
}
this.messageLen = message.length;
CRC32 crc32 = new CRC32();
crc32.update(message);
this.checksum = crc32.getValue();
this.fragmentLen = findNominalFragmentLength(messageLen, minFragmentLen, maxFragmentLen);
this.fragments = partitionMessage(message, fragmentLen);
this.seqLen = fragments.size();
this.seqNum = firstSeqNum;
}
public Part nextPart() {
seqNum += 1;
partIndexes = chooseFragments(seqNum, seqLen, checksum);
byte[] mixed = mix(partIndexes);
return new Part(seqNum, seqLen, messageLen, checksum, mixed);
}
private byte[] mix(List<Integer> partIndexes) {
return partIndexes.stream().reduce(new byte[fragmentLen], (result, index) -> xor(fragments.get(index), result), FountainEncoder::xor);
}
public static byte[] xor(byte[] a, byte[] b) {
byte[] result = new byte[a.length];
for (int i = 0; i < result.length; i++) {
result[i] = (byte) (((int) a[i]) ^ ((int) b[i]));
}
return result;
}
public boolean isComplete() {
return seqNum >= seqLen;
}
public boolean isSinglePart() {
return seqLen == 1;
}
public long getSeqNum() {
return seqNum;
}
public int getSeqLen() {
return seqLen;
}
public List<Integer> getPartIndexes() {
return partIndexes;
}
static List<byte[]> partitionMessage(byte[] message, int fragmentLen) {
int fragmentCount = (int)Math.ceil(message.length / (double)fragmentLen);
List<byte[]> fragments = new ArrayList<>();
int start = 0;
for(int i = 0; i < fragmentCount; i++) {
fragments.add(Arrays.copyOfRange(message, start, start + fragmentLen));
start += fragmentLen;
}
return fragments;
}
static int findNominalFragmentLength(int messageLen, int minFragmentLen, int maxFragmentLen) {
int maxFragmentCount = messageLen / minFragmentLen;
int fragmentLen = 0;
for(int fragmentCount = 1; fragmentCount <= maxFragmentCount; fragmentCount++) {
fragmentLen = (int)Math.ceil((double)messageLen / (double)fragmentCount);
if(fragmentLen <= maxFragmentLen) {
break;
}
}
return fragmentLen;
}
public static class Part {
private final long seqNum;
private final int seqLen;
private final int messageLen;
private final long checksum;
private final byte[] data;
public Part(long seqNum, int seqLen, int messageLen, long checksum, byte[] data) {
this.seqNum = seqNum;
this.seqLen = seqLen;
this.messageLen = messageLen;
this.checksum = checksum;
this.data = data;
}
public long getSeqNum() {
return seqNum;
}
public int getSeqLen() {
return seqLen;
}
public int getMessageLen() {
return messageLen;
}
public long getChecksum() {
return checksum;
}
public byte[] getData() {
return data;
}
public byte[] toCborBytes() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new CborEncoder(baos).encode(new CborBuilder()
.addArray()
.add(new UnsignedInteger(seqNum))
.add(new UnsignedInteger(seqLen))
.add(new UnsignedInteger(messageLen))
.add(new UnsignedInteger(checksum))
.add(data)
.end()
.build());
return baos.toByteArray();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
public static Part fromCborBytes(byte[] cborData) throws CborException {
ByteArrayInputStream bais = new ByteArrayInputStream(cborData);
List<DataItem> arrayDataItems = new CborDecoder(bais).decode();
Array array = (Array)arrayDataItems.get(0);
List<DataItem> dataItems = array.getDataItems();
UnsignedInteger seqNum = (UnsignedInteger)dataItems.get(0);
UnsignedInteger seqLen = (UnsignedInteger)dataItems.get(1);
UnsignedInteger messageLen = (UnsignedInteger)dataItems.get(2);
UnsignedInteger checksum = (UnsignedInteger)dataItems.get(3);
ByteString data = (ByteString)dataItems.get(4);
return new Part(seqNum.getValue().longValue(), seqLen.getValue().intValue(), messageLen.getValue().intValue(), checksum.getValue().longValue(), data.getBytes());
}
}
}
@@ -0,0 +1,47 @@
package com.sparrowwallet.sparrow.ur.fountain;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class FountainUtils {
static List<Integer> chooseFragments(long seqNum, int seqLen, long checkSum) {
// The first `seqLen` parts are the "pure" fragments, not mixed with any
// others. This means that if you only generate the first `seqLen` parts,
// then you have all the parts you need to decode the message.
if(seqNum <= seqLen) {
return List.of((int)seqNum - 1);
} else {
ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES * 2);
buffer.putInt((int)(seqNum));
buffer.putInt((int)(checkSum));
RandomXoshiro256StarStar rng = new RandomXoshiro256StarStar(buffer.array());
int degree = chooseDegree(seqLen, rng);
List<Integer> indexes = IntStream.range(0, seqLen).boxed().collect(Collectors.toList());
List<Integer> shuffledIndexes = shuffled(indexes, rng);
return new ArrayList<>(shuffledIndexes.subList(0, degree));
}
}
static int chooseDegree(int seqLen, RandomXoshiro256StarStar rng) {
List<Double> degreeProbabilties = IntStream.range(1, seqLen + 1).mapToObj(i -> 1 / (double)i).collect(Collectors.toList());
AliasMethod degreeChooser = new AliasMethod(degreeProbabilties, rng);
return degreeChooser.next() + 1;
}
static List<Integer> shuffled(List<Integer> indexes, RandomXoshiro256StarStar rng) {
List<Integer> remaining = new ArrayList<>(indexes);
List<Integer> shuffled = new ArrayList<>(indexes.size());
while(!remaining.isEmpty()) {
int index = rng.nextInt(0, remaining.size());
Integer item = remaining.remove(index);
shuffled.add(item);
}
return shuffled;
}
}
@@ -0,0 +1,241 @@
package com.sparrowwallet.sparrow.ur.fountain;
/*
* To the extent possible under law, the author has dedicated all copyright
* and related and neighboring rights to this software to the public domain
* worldwide. This software is distributed without any warranty.
*
* See <http://creativecommons.org/publicdomain/zero/1.0/>
*/
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import java.nio.charset.StandardCharsets;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
/**
* Implementation of Random based on the xoshiro256** RNG. No-dependencies
* Java port of the <a href="http://xoshiro.di.unimi.it/xoshiro256starstar.c">original C code</a>,
* which is public domain. This Java port is similarly dedicated to the public
* domain.
* <p>
* Individual instances are not thread-safe. Each thread must have its own
* instance which is not shared.
*
* @author David Blackman and Sebastiano Vigna &lt;vigna@acm.org> (original C code)
* @author Una Thompson &lt;una@unascribed.com> (Java port)
* @see <a href="http://xoshiro.di.unimi.it/">http://xoshiro.di.unimi.it/</a>
*/
public class RandomXoshiro256StarStar extends Random {
private static final long serialVersionUID = -2837799889588687855L;
private static final AtomicLong uniq = new AtomicLong(System.nanoTime());
private static final long nextUniq() {
return splitmix64_2(uniq.addAndGet(SPLITMIX1_MAGIC));
}
private long seed;
public RandomXoshiro256StarStar() {
this(System.nanoTime() ^ nextUniq());
}
public RandomXoshiro256StarStar(long seed) {
super(seed);
// super will call setSeed
}
public RandomXoshiro256StarStar(String seed) {
this(seed.getBytes(StandardCharsets.UTF_8));
}
public RandomXoshiro256StarStar(byte[] seed) {
this(Sha256Hash.of(seed));
}
public RandomXoshiro256StarStar(Sha256Hash digest) {
long[] s = new long[4];
byte[] digestBytes = digest.getBytes();
for(int i = 0; i < 4; i++) {
int o = i * 8;
long v = 0L;
for(int n = 0; n < 8; n++) {
v = v << 8;
v |= digestBytes[o + n] & 0xFF;
}
s[i] = v;
}
setState(s[0], s[1], s[2], s[3]);
}
public RandomXoshiro256StarStar(long s1, long s2, long s3, long s4) {
setState(s1, s2, s3, s4);
}
// used to "stretch" seeds into a full 256-bit state; also makes
// it safe to pass in zero as a seed
////
// what generator is used here is unimportant, as long as it's
// from a different family, but splitmix64 happens to be an
// incredibly simple high-quality generator of a completely
// different family (and is recommended by the xoshiro authors)
private static final long SPLITMIX1_MAGIC = 0x9E3779B97F4A7C15L;
private static long splitmix64_1(long x) {
return (x + SPLITMIX1_MAGIC);
}
private static long splitmix64_2(long z) {
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9L;
z = (z ^ (z >> 27)) * 0x94D049BB133111EBL;
return z ^ (z >> 31);
}
@Override
public void setSeed(long seed) {
this.seed = seed;
// update haveNextNextGaussian flag in super
super.setSeed(seed);
long sms = splitmix64_1(seed);
s0 = splitmix64_2(sms);
sms = splitmix64_1(sms);
s1 = splitmix64_2(sms);
sms = splitmix64_1(sms);
s2 = splitmix64_2(sms);
sms = splitmix64_1(sms);
s3 = splitmix64_2(sms);
}
public void setState(long s0, long s1, long s2, long s4) {
if(s0 == 0 && s1 == 0 && s2 == 0 && s4 == 0) {
throw new IllegalArgumentException("xoshiro256** state cannot be all zeroes");
}
this.s0 = s0;
this.s1 = s1;
this.s2 = s2;
this.s3 = s4;
}
// not called, implemented instead of just throwing for completeness
@Override
protected int next(int bits) {
return (int) (nextLong() & ((1L << bits) - 1));
}
@Override
public int nextInt() {
return (int) nextLong();
}
@Override
public int nextInt(int bound) {
return (int) nextLong(bound);
}
public long nextLong(long bound) {
if(bound <= 0) {
throw new IllegalArgumentException("bound must be positive");
}
// clear sign bit for positive-only, modulo to bound
return (nextLong() & Long.MAX_VALUE) % bound;
}
@Override
public double nextDouble() {
return (nextLong() >>> 11) * 0x1.0P-53;
}
@Override
public float nextFloat() {
return (nextLong() >>> 40) * 0x1.0P-24f;
}
@Override
public boolean nextBoolean() {
return (nextLong() & 1) != 0;
}
@Override
public void nextBytes(byte[] buf) {
nextBytes(buf, 0, buf.length);
}
public void nextBytes(byte[] buf, int ofs, int len) {
if(ofs < 0) {
throw new ArrayIndexOutOfBoundsException("Offset " + ofs + " is negative");
}
if(ofs >= buf.length) {
throw new ArrayIndexOutOfBoundsException("Offset " + ofs + " is greater than buffer length");
}
if(ofs + len > buf.length) {
throw new ArrayIndexOutOfBoundsException("Length " + len + " with offset " + ofs + " is past end of buffer");
}
int j = 8;
long l = 0;
for(int i = ofs; i < ofs + len; i++) {
if(j >= 8) {
l = nextLong();
j = 0;
}
buf[i] = (byte) (l & 0xFF);
l = l >>> 8L;
j++;
}
}
public void nextData(byte[] data) {
for(int i = 0; i < data.length; i++) {
data[i] = (byte)(nextInt(0, 256) & 0xFF);
}
}
public int nextInt(int lowerBound, int count) {
double next = nextDouble();
double dou = (next * count);
return (int)(dou) + lowerBound;
}
/* This is xoshiro256** 1.0, our all-purpose, rock-solid generator. It has
excellent (sub-ns) speed, a state (256 bits) that is large enough for
any parallel application, and it passes all tests we are aware of.
For generating just floating-point numbers, xoshiro256+ is even faster.
The state must be seeded so that it is not everywhere zero. If you have
a 64-bit seed, we suggest to seed a splitmix64 generator and use its
output to fill s. */
private static long rotl(long x, int k) {
return (x << k) | (x >>> (64 - k));
}
private long s0;
private long s1;
private long s2;
private long s3;
@Override
public long nextLong() {
long result_starstar = rotl(s1 * 5, 7) * 9;
long t = s1 << 17;
s2 ^= s0;
s3 ^= s1;
s1 ^= s2;
s0 ^= s3;
s2 ^= t;
s3 = rotl(s3, 45);
return result_starstar;
}
}
+1
View File
@@ -17,6 +17,7 @@ open module com.sparrowwallet.sparrow {
requires simple.json.rpc.core;
requires org.jetbrains.annotations;
requires com.fasterxml.jackson.databind;
requires cbor;
requires centerdevice.nsmenufx;
requires javafx.swing;
}