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

View file

@ -37,6 +37,7 @@ dependencies {
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
exclude group: 'org.slf4j'
}
implementation('co.nstant.in:cbor:0.9')
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('org.controlsfx:controlsfx:11.0.1' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
@ -79,12 +80,12 @@ run {
jlink {
mergedModule {
requires 'javafx.graphics';
requires 'javafx.controls';
requires 'java.xml';
requires 'javafx.graphics'
requires 'javafx.controls'
requires 'java.xml'
requires 'java.logging'
requires 'javafx.base';
requires 'com.fasterxml.jackson.databind';
requires 'javafx.base'
requires 'com.fasterxml.jackson.databind'
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png']

View file

@ -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);
}
}
}

View file

@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.ur;
public enum ResultType {
SUCCESS, FAILURE;
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -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];
}
}

View file

@ -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;
}
}

View file

@ -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());
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

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;
}

View file

@ -0,0 +1,24 @@
package com.sparrowwallet.sparrow.ur;
import com.sparrowwallet.drongo.Utils;
import org.junit.Assert;
import org.junit.Test;
public class BytewordsTest {
@Test
public void test() {
byte[] data = Utils.hexToBytes("d9012ca20150c7098580125e2ab0981253468b2dbc5202d8641947da");
String encoded = Bytewords.encode(data, Bytewords.Style.STANDARD);
Assert.assertEquals("tuna acid draw oboe acid good slot axis list lava brag holy door puff monk brag guru frog luau drop roof grim also trip idle chef fuel twin tied draw grim ramp", encoded);
byte[] data2 = Bytewords.decode(encoded, Bytewords.Style.STANDARD);
Assert.assertArrayEquals(data, data2);
encoded = Bytewords.encode(data, Bytewords.Style.URI);
Assert.assertEquals("tuna-acid-draw-oboe-acid-good-slot-axis-list-lava-brag-holy-door-puff-monk-brag-guru-frog-luau-drop-roof-grim-also-trip-idle-chef-fuel-twin-tied-draw-grim-ramp", encoded);
encoded = Bytewords.encode(data, Bytewords.Style.MINIMAL);
Assert.assertEquals("taaddwoeadgdstasltlabghydrpfmkbggufgludprfgmaotpiecffltntddwgmrp", encoded);
data2 = Bytewords.decode(encoded, Bytewords.Style.MINIMAL);
Assert.assertArrayEquals(data, data2);
}
}

View file

@ -0,0 +1,140 @@
package com.sparrowwallet.sparrow.ur;
import co.nstant.in.cbor.CborBuilder;
import co.nstant.in.cbor.CborEncoder;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.sparrow.ur.fountain.FountainEncoder;
import com.sparrowwallet.sparrow.ur.fountain.RandomXoshiro256StarStar;
import org.junit.Assert;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class URTest {
@Test
public void testSinglePartUR() {
UR ur = makeMessageUR(50, "Wolf");
String encoded = UREncoder.encode(ur);
Assert.assertEquals("ur:bytes/hdeymejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtgwdpfnsboxgwlbaawzuefywkdplrsrjynbvygabwjldapfcsdwkbrkch", encoded);
}
@Test
public void testEncode() {
UR ur = makeMessageUR(256, "Wolf");
UREncoder urEncoder = new UREncoder(ur, 30, 10, 0);
List<String> parts = IntStream.range(0, 20).mapToObj(i -> urEncoder.nextPart()).collect(Collectors.toList());
String[] expectedParts = new String[] {
"ur:bytes/1-9/ltadascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtdkgsltgh",
"ur:bytes/2-9/ltaoascfadaxcywenbpljkhdcagwdpfnsboxgwlbaawzuefywkdplrsrjynbvygabwjldapfcsgmghhkhstlrdcxaefz",
"ur:bytes/3-9/ltaxascfadaxcywenbpljkhdcahelbknlkuejnbadmssfhfrdpsbiegecpasvssovlgeykssjykklronvsjksopdzool",
"ur:bytes/4-9/ltaaascfadaxcywenbpljkhdcasotkhemthydawydtaxneurlkosgwcekonertkbrlwmplssjtammdplolsbrdzertas",
"ur:bytes/5-9/ltahascfadaxcywenbpljkhdcatbbdfmssrkzocwnezmlennjpfzbgmuktrhtejscktelgfpdlrkfyfwdajldejokbwf",
"ur:bytes/6-9/ltamascfadaxcywenbpljkhdcackjlhkhybssklbwefectpfnbbectrljectpavyrolkzezepkmwidmwoxkilghdsowp",
"ur:bytes/7-9/ltatascfadaxcywenbpljkhdcavszownjkwtclrtvaynhpahrtoxmwvwatmedibkaegdosftvandiodagdhthtrlnnhy",
"ur:bytes/8-9/ltayascfadaxcywenbpljkhdcadmsponkkbbhgsolnjntegepmttmoonftnbuoiyrehfrtsabzsttorodklubbuyaetk",
"ur:bytes/9-9/ltasascfadaxcywenbpljkhdcajskecpmdckihdyhphfotjojtfmlpwmadspaxrkytbztpbauotbgtgtaeaevtgavtny",
"ur:bytes/10-9/ltbkascfadaxcywenbpljkhdcazoqdayfeaavsnnrffhjnfytplguytsoyspgdrhluheihtyettewtytcfrtdeahhdad",
"ur:bytes/11-9/ltbdascfadaxcywenbpljkhdcavdintbiyjltafyknfspefrvdondtvlgdckfslthkgtghsbsbbtiyechthdlakobtfd",
"ur:bytes/12-9/ltbnascfadaxcywenbpljkhdcalndikttpecueksoecypdssvtplkiryjydioefywyrtjlsedppagwpseturfhbzmdmd",
"ur:bytes/13-9/ltbtascfadaxcywenbpljkhdcazoqdayfeaavsnnrffhjnfytplguytsoyspgdrhluheihtyettewtytcfrtutwtoyon",
"ur:bytes/14-9/ltbaascfadaxcywenbpljkhdcanbsfjpsotnltmhmoztgmlbykfgrsntlsserojoisbzmhbegspkjyhhwnqdfneobkfd",
"ur:bytes/15-9/ltbsascfadaxcywenbpljkhdcaynmhpddpzoversbdqdfyrehnqzlugmjzmnmtwmrouohtstgsbsahpawkditkckynwt",
"ur:bytes/16-9/ltbeascfadaxcywenbpljkhdcazoqdayfeaavsnnrffhjnfytplguytsoyspgdrhluheihtyettewtytcfrtghtduycm",
"ur:bytes/17-9/ltbyascfadaxcywenbpljkhdcarpfeneknvyyadifltalghskpgrfgsngulagspfpthyrpgrsoatjnuyflvsdmpyinmw",
"ur:bytes/18-9/ltbgascfadaxcywenbpljkhdcavtrfmwktecjnnsyafsemaaspglynhhrhmyjyoelgjtpyhkssamdsfehfnsfrcfrnyk",
"ur:bytes/19-9/ltbwascfadaxcywenbpljkhdcaenbgkghtlbiybwfpjlbyecmoythnmesbkopahtiofywnutvacfhdjyiobwrtlbtbme",
"ur:bytes/20-9/ltbbascfadaxcywenbpljkhdcarssrwyztwmaemotbytayfhvwltmocmndlpnsjejtdkhyntpflboevtrnwsdkjssbrs"
};
Assert.assertArrayEquals("", expectedParts, parts.toArray());
}
@Test
public void testMultipartUR() {
UR ur = makeMessageUR(32767, "Wolf");
int maxFragmentLen = 1000;
UREncoder urEncoder = new UREncoder(ur, maxFragmentLen, 10, 100);
URDecoder urDecoder = new URDecoder();
do {
String part = urEncoder.nextPart();
urDecoder.receivePart(part);
} while(urDecoder.getResult() == null);
Assert.assertEquals(ResultType.SUCCESS, urDecoder.getResult().type);
UR decodedUR = urDecoder.getResult().ur;
Assert.assertEquals(ur, decodedUR);
}
@Test
public void testEncoderCBOR() {
byte[] message = makeMessage(256, "Wolf");
FountainEncoder fountainEncoder = new FountainEncoder(message, 30, 10, 0);
FountainEncoder.Part[] parts = IntStream.range(0, 20).mapToObj(i -> fountainEncoder.nextPart()).collect(Collectors.toList()).toArray(new FountainEncoder.Part[20]);
List<String> partsHex = Arrays.stream(parts).map(part -> Utils.bytesToHex(part.toCborBytes())).collect(Collectors.toList());
String[] expectedPartsHex = new String[] {
"8501091901001a0167aa07581d916ec65cf77cadf55cd7f9cda1a1030026ddd42e905b77adc36e4f2d3c",
"8502091901001a0167aa07581dcba44f7f04f2de44f42d84c374a0e149136f25b01852545961d55f7f7a",
"8503091901001a0167aa07581d8cde6d0e2ec43f3b2dcb644a2209e8c9e34af5c4747984a5e873c9cf5f",
"8504091901001a0167aa07581d965e25ee29039fdf8ca74f1c769fc07eb7ebaec46e0695aea6cbd60b3e",
"8505091901001a0167aa07581dc4bbff1b9ffe8a9e7240129377b9d3711ed38d412fbb4442256f1e6f59",
"8506091901001a0167aa07581d5e0fc57fed451fb0a0101fb76b1fb1e1b88cfdfdaa946294a47de8fff1",
"8507091901001a0167aa07581d73f021c0e6f65b05c0a494e50791270a0050a73ae69b6725505a2ec8a5",
"8508091901001a0167aa07581d791457c9876dd34aadd192a53aa0dc66b556c0c215c7ceb8248b717c22",
"8509091901001a0167aa07581d951e65305b56a3706e3e86eb01c803bbf915d80edcd64d4d0000000000",
"850a091901001a0167aa07581d4a1b58fa2733399e5ee04d87a2d1628186e3cd250f3ae0e25d7ae7a22b",
"850b091901001a0167aa07581dd35acd70953cf29b542a94cbd75790c73cb4cb1056d56557bf0b70b936",
"850c091901001a0167aa07581d8cde6d0e2ec43f3b2dcb644a2209e8c9e34af5c4747984a5e873c9cf5f",
"850d091901001a0167aa07581d760be7ad1c6187902bbc04f539b9ee5eb8ea6833222edea36031306c01",
"850e091901001a0167aa07581dcba44f7f04f2de44f42d84c374a0e149136f25b01852545961d55f7f7a",
"850f091901001a0167aa07581d262518878e747c6eee337fbbd189f77b385efe55597b54cab65b7f8ac0",
"8510091901001a0167aa07581d2d4a0b8fb95226315ab796cd72f9c5f8ea2a5a84221f7e31318f71b7df",
"8511091901001a0167aa07581d81bf178523c1e7daaacdc944d67183c8958ce8951768b4bb3cafb8dd51",
"8512091901001a0167aa07581d8e1548d10a2cd18416a3428bcb1fe2e3cac640274e91df20bdbd4e4df9",
"8513091901001a0167aa07581d8a65ebbda606df01244a3dad6b76e258150e4c07021ce5c07ed30160c5",
"8514091901001a0167aa07581d2b44620c8371a48f6935d2b525f19c7a4e98e3043a6a64d462870e98ce"};
Assert.assertEquals(Arrays.asList(expectedPartsHex), partsHex);
}
public static byte[] makeMessage(int len, String seed) {
RandomXoshiro256StarStar rng = new RandomXoshiro256StarStar(seed);
byte[] message = new byte[len];
rng.nextData(message);
return message;
}
private UR makeMessageUR(int len, String seed) {
try {
byte[] message = makeMessage(len, seed);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new CborEncoder(baos).encode(new CborBuilder()
.add(message)
.build());
byte[] cbor = baos.toByteArray();
return new UR("bytes", cbor);
} catch(Exception e) {
throw new RuntimeException(e);
}
}
@Test
public void testCbor() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new CborEncoder(baos).encode(new CborBuilder()
.add(Utils.hexToBytes("00112233445566778899aabbccddeeff"))
.build());
byte[] cbor = baos.toByteArray();
Assert.assertEquals("5000112233445566778899aabbccddeeff", Utils.bytesToHex(cbor));
UR ur = new UR("bytes", cbor);
String encoded = UREncoder.encode(ur);
Assert.assertEquals("ur:bytes/gdaebycpeofygoiyktlonlpkrksfutwyzowmfyeozs", encoded);
}
}

View file

@ -0,0 +1,151 @@
package com.sparrowwallet.sparrow.ur.fountain;
import co.nstant.in.cbor.CborException;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.sparrow.ur.ResultType;
import com.sparrowwallet.sparrow.ur.URTest;
import org.junit.Assert;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class FountainCodesTest {
@Test
public void testRNG3() {
RandomXoshiro256StarStar rng = new RandomXoshiro256StarStar("Wolf");
int[] numbers = IntStream.range(0, 100).map(i -> rng.nextInt(1, 10)).toArray();
int[] expectedNumbers = new int[] {6, 5, 8, 4, 10, 5, 7, 10, 4, 9, 10, 9, 7, 7, 1, 1, 2, 9, 9, 2, 6, 4, 5, 7, 8, 5, 4, 2, 3, 8, 7, 4, 5, 1, 10, 9, 3, 10, 2, 6, 8, 5, 7, 9, 3, 1, 5, 2, 7, 1, 4, 4, 4, 4, 9, 4, 5, 5, 6, 9, 5, 1, 2, 8, 3, 3, 2, 8, 4, 3, 2, 1, 10, 8, 9, 3, 10, 8, 5, 5, 6, 7, 10, 5, 8, 9, 4, 6, 4, 2, 10, 2, 1, 7, 9, 6, 7, 4, 2, 5};
Assert.assertArrayEquals(expectedNumbers, numbers);
}
@Test
public void testRandomSampler() {
RandomXoshiro256StarStar rng = new RandomXoshiro256StarStar("Wolf");
AliasMethod aliasMethod = new AliasMethod(List.of(1d, 2d, 4d, 8d), rng);
int[] numbers = IntStream.range(0, 500).map(i -> aliasMethod.next()).toArray();
int[] expectedNumbers = new int[] {2, 1, 2, 0, 2, 2, 0, 1, 0, 1, 1, 2, 1, 2, 3, 3, 1, 2, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 3, 0, 1, 0, 2, 0, 1, 3, 3, 0, 3, 1, 2, 1, 2, 2, 0, 3, 2, 3, 2, 3, 1, 3, 1, 1, 0, 0, 1, 0, 0, 3, 0, 0, 2, 1, 2, 3, 3, 3, 3, 2, 0, 2, 3, 0, 0, 3, 1, 0, 2, 1, 1, 3, 0, 0, 2, 1, 1, 3, 3, 1, 3, 0, 1, 1, 0, 1, 0, 0, 1, 0, 3, 2, 2, 2, 1, 1, 0, 1, 3, 1, 0, 3, 3, 1, 3, 2, 1, 2, 2, 1, 3, 3, 3, 3, 2, 0, 0, 2, 2, 0, 2, 2, 1, 3, 2, 1, 2, 2, 2, 3, 0, 2, 1, 3, 1, 3, 1, 3, 0, 2, 2, 3, 2, 3, 1, 1, 1, 2, 3, 0, 1, 2, 3, 1, 2, 2, 1, 3, 3, 3, 2, 1, 0, 1, 1, 3, 2, 2, 3, 0, 0, 2, 0, 1, 0, 2, 2, 2, 1, 0, 2, 1, 2, 1, 3, 0, 0, 1, 0, 0, 0, 0, 1, 2, 1, 0, 1, 3, 1, 1, 3, 2, 1, 0, 2, 2, 0, 1, 1, 3, 0, 3, 3, 0, 1, 3, 3, 1, 2, 1, 1, 1, 2, 3, 2, 2, 1, 3, 2, 1, 3, 0, 2, 2, 0, 1, 3, 3, 0, 1, 1, 2, 3, 0, 2, 3, 1, 2, 1, 0, 2, 2, 0, 1, 2, 1, 3, 3, 0, 0, 3, 1, 2, 2, 0, 0, 2, 1, 1, 3, 2, 0, 3, 0, 0, 3, 0, 0, 3, 2, 2, 3, 3, 3, 3, 3, 3, 2, 1, 2, 2, 0, 2, 3, 3, 1, 3, 2, 1, 3, 2, 0, 0, 0, 0, 1, 0, 3, 1, 1, 1, 0, 0, 3, 0, 1, 1, 3, 2, 3, 3, 3, 2, 2, 2, 2, 0, 2, 2, 0, 2, 3, 2, 0, 3, 2, 2, 3, 3, 0, 3, 0, 2, 1, 2, 1, 0, 2, 2, 0, 0, 0, 0, 3, 0, 3, 0, 2, 3, 0, 3, 3, 3, 3, 3, 2, 3, 1, 2, 1, 0, 3, 0, 0, 1, 3, 0, 0, 0, 1, 3, 0, 3, 1, 0, 3, 2, 3, 0, 0, 1, 1, 3, 1, 3, 3, 1, 1, 2, 3, 3, 0, 0, 0, 2, 2, 2, 2, 1, 0, 2, 2, 3, 2, 2, 0, 2, 3, 2, 0, 3, 0, 1, 2, 2, 0, 2, 3, 0, 0, 2, 0, 3, 0, 1, 1, 3, 2, 2, 2, 0, 1, 2, 3, 3, 2, 2, 1, 2, 3, 1, 1, 1, 0, 3, 3, 0, 1, 2, 1, 0, 3, 2, 0, 3, 1, 1, 2, 2, 0, 1, 3, 0, 1, 3, 0, 0, 2, 0, 3, 0, 1, 2, 2, 0, 3, 1, 2, 0, 2};
Assert.assertArrayEquals(expectedNumbers, numbers);
}
@Test
public void testShuffle() {
RandomXoshiro256StarStar rng = new RandomXoshiro256StarStar("Wolf");
List<Integer> numbers = IntStream.range(1, 11).boxed().collect(Collectors.toList());
numbers = FountainUtils.shuffled(numbers, rng);
List<Integer> expectedNumbers = List.of(6, 4, 9, 3, 10, 5, 7, 8, 1, 2);
Assert.assertEquals(expectedNumbers, numbers);
}
@Test
public void testXOR() {
RandomXoshiro256StarStar rng = new RandomXoshiro256StarStar("Wolf");
byte[] data1 = new byte[10];
rng.nextData(data1);
Assert.assertEquals("916ec65cf77cadf55cd7", Utils.bytesToHex(data1));
byte[] data2 = new byte[10];
rng.nextData(data2);
Assert.assertEquals("f9cda1a1030026ddd42e", Utils.bytesToHex(data2));
byte[] data3 = FountainEncoder.xor(data1, data2);
Assert.assertEquals("68a367fdf47c8b2888f9", Utils.bytesToHex(data3));
}
@Test
public void testEncoderCBOR() {
byte[] message = URTest.makeMessage(256, "Wolf");
FountainEncoder encoder = new FountainEncoder(message, 30, 10, 0);
List<FountainEncoder.Part> parts = IntStream.range(0, 20).mapToObj(i -> encoder.nextPart()).collect(Collectors.toList());
List<String> partsHex = parts.stream().map(part -> Utils.bytesToHex(part.toCborBytes())).collect(Collectors.toList());
String[] expectedPartsHex = new String[] {
"8501091901001a0167aa07581d916ec65cf77cadf55cd7f9cda1a1030026ddd42e905b77adc36e4f2d3c",
"8502091901001a0167aa07581dcba44f7f04f2de44f42d84c374a0e149136f25b01852545961d55f7f7a",
"8503091901001a0167aa07581d8cde6d0e2ec43f3b2dcb644a2209e8c9e34af5c4747984a5e873c9cf5f",
"8504091901001a0167aa07581d965e25ee29039fdf8ca74f1c769fc07eb7ebaec46e0695aea6cbd60b3e",
"8505091901001a0167aa07581dc4bbff1b9ffe8a9e7240129377b9d3711ed38d412fbb4442256f1e6f59",
"8506091901001a0167aa07581d5e0fc57fed451fb0a0101fb76b1fb1e1b88cfdfdaa946294a47de8fff1",
"8507091901001a0167aa07581d73f021c0e6f65b05c0a494e50791270a0050a73ae69b6725505a2ec8a5",
"8508091901001a0167aa07581d791457c9876dd34aadd192a53aa0dc66b556c0c215c7ceb8248b717c22",
"8509091901001a0167aa07581d951e65305b56a3706e3e86eb01c803bbf915d80edcd64d4d0000000000",
"850a091901001a0167aa07581d4a1b58fa2733399e5ee04d87a2d1628186e3cd250f3ae0e25d7ae7a22b",
"850b091901001a0167aa07581dd35acd70953cf29b542a94cbd75790c73cb4cb1056d56557bf0b70b936",
"850c091901001a0167aa07581d8cde6d0e2ec43f3b2dcb644a2209e8c9e34af5c4747984a5e873c9cf5f",
"850d091901001a0167aa07581d760be7ad1c6187902bbc04f539b9ee5eb8ea6833222edea36031306c01",
"850e091901001a0167aa07581dcba44f7f04f2de44f42d84c374a0e149136f25b01852545961d55f7f7a",
"850f091901001a0167aa07581d262518878e747c6eee337fbbd189f77b385efe55597b54cab65b7f8ac0",
"8510091901001a0167aa07581d2d4a0b8fb95226315ab796cd72f9c5f8ea2a5a84221f7e31318f71b7df",
"8511091901001a0167aa07581d81bf178523c1e7daaacdc944d67183c8958ce8951768b4bb3cafb8dd51",
"8512091901001a0167aa07581d8e1548d10a2cd18416a3428bcb1fe2e3cac640274e91df20bdbd4e4df9",
"8513091901001a0167aa07581d8a65ebbda606df01244a3dad6b76e258150e4c07021ce5c07ed30160c5",
"8514091901001a0167aa07581d2b44620c8371a48f6935d2b525f19c7a4e98e3043a6a64d462870e98ce"
};
Assert.assertEquals(Arrays.asList(expectedPartsHex), partsHex);
}
@Test
public void testEncoderIsComplete() {
byte[] message = URTest.makeMessage(256, "Wolf");
FountainEncoder encoder = new FountainEncoder(message, 30, 10, 0);
int generatedPartsCount = 0;
while(!encoder.isComplete()) {
encoder.nextPart();
generatedPartsCount++;
}
Assert.assertEquals(encoder.getSeqLen(), generatedPartsCount);
}
@Test
public void testDecoder() {
String messageSeed = "Wolf";
int messageSize = 32767;
int maxFragmentLen = 1000;
byte[] message = URTest.makeMessage(messageSize, messageSeed);
FountainEncoder encoder = new FountainEncoder(message, maxFragmentLen, 10, 0);
FountainDecoder decoder = new FountainDecoder();
do {
FountainEncoder.Part part = encoder.nextPart();
decoder.receivePart(part);
} while(decoder.getResult() == null);
Assert.assertEquals(ResultType.SUCCESS, decoder.getResult().type);
Assert.assertArrayEquals(message, decoder.getResult().data);
}
@Test
public void testDecoderHighFirstSeq() {
String messageSeed = "Wolf";
int messageSize = 32767;
int maxFragmentLen = 1000;
byte[] message = URTest.makeMessage(messageSize, messageSeed);
FountainEncoder encoder = new FountainEncoder(message, maxFragmentLen, 10, 100);
FountainDecoder decoder = new FountainDecoder();
do {
FountainEncoder.Part part = encoder.nextPart();
decoder.receivePart(part);
} while(decoder.getResult() == null);
Assert.assertEquals(ResultType.SUCCESS, decoder.getResult().type);
Assert.assertArrayEquals(message, decoder.getResult().data);
}
@Test
public void testCBOR() throws CborException {
FountainEncoder.Part part = new FountainEncoder.Part(12, 8, 100, 0x12345678, new byte[] {1,5,3,3,5});
byte[] cbor = part.toCborBytes();
FountainEncoder.Part part2 = FountainEncoder.Part.fromCborBytes(cbor);
Assert.assertArrayEquals(cbor, part2.toCborBytes());
}
}