qr code display and random sampler port

This commit is contained in:
Craig Raw 2020-08-03 10:30:30 +02:00
parent 4068a6c541
commit 709c65ec20
8 changed files with 322 additions and 208 deletions

View file

@ -0,0 +1,112 @@
package com.sparrowwallet.sparrow.control;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.client.j2se.MatrixToImageConfig;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.ur.UR;
import com.sparrowwallet.sparrow.ur.UREncoder;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.controlsfx.tools.Borders;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
public class QRDisplayDialog extends Dialog<UR> {
private static final int MIN_FRAGMENT_LENGTH = 10;
private static final int MAX_FRAGMENT_LENGTH = 100;
private final UR ur;
private final UREncoder encoder;
private final ImageView qrImageView;
private String currentPart;
public QRDisplayDialog(byte[] data) {
this(UR.fromBytes(data));
}
public QRDisplayDialog(UR ur) {
this.ur = ur;
this.encoder = new UREncoder(ur, MAX_FRAGMENT_LENGTH, MIN_FRAGMENT_LENGTH, 0);
EventManager.get().register(this);
final DialogPane dialogPane = getDialogPane();
StackPane stackPane = new StackPane();
qrImageView = new ImageView();
stackPane.getChildren().add(qrImageView);
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().outerPadding(0).innerPadding(0).buildAll());
nextPart();
if(encoder.isSinglePart()) {
qrImageView.setImage(getQrCode(currentPart));
} else {
AnimateQRService animateQRService = new AnimateQRService();
animateQRService.setPeriod(Duration.millis(100));
animateQRService.start();
setOnCloseRequest(event -> {
animateQRService.cancel();
});
}
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(550);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null);
}
private void nextPart() {
String fragment = encoder.nextPart();
currentPart = fragment.toUpperCase();
}
private Image getQrCode(String fragment) {
try {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, 480, 480);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(qrMatrix, "PNG", baos, new MatrixToImageConfig());
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
return new Image(bais);
} catch(Exception e) {
e.printStackTrace();
}
return null;
}
private class AnimateQRService extends ScheduledService<Boolean> {
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws ImportException {
Image qrImage = getQrCode(currentPart);
qrImageView.setImage(qrImage);
nextPart();
return true;
}
};
}
}
}

View file

@ -530,7 +530,8 @@ public class HeadersController extends TransactionFormController implements Init
ToggleButton toggleButton = (ToggleButton)event.getSource();
toggleButton.setSelected(false);
headersForm.getSignedKeystores().add(headersForm.getSigningWallet().getKeystores().get(0));
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(headersForm.getPsbt().serialize());
qrDisplayDialog.show();
}
public void scanPSBT(ActionEvent event) {

View file

@ -43,6 +43,14 @@ public class UR {
return false;
}
public static UR fromBytes(byte[] data) {
try {
return new UR("bytes", data);
} catch(UR.InvalidTypeException e) {
return null;
}
}
@Override
public boolean equals(Object o) {
if(this == o) {

View file

@ -1,149 +0,0 @@
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

@ -31,8 +31,8 @@ public class FountainUtils {
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;
RandomSampler randomSampler = new RandomSampler(degreeProbabilties);
return randomSampler.next(rng) + 1;
}
static List<Integer> shuffled(List<Integer> indexes, RandomXoshiro256StarStar rng) {

View file

@ -0,0 +1,84 @@
package com.sparrowwallet.sparrow.ur.fountain;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
/**
* Random-number sampling using the Walker-Vose alias method,
* as described by Keith Schwarz (2011)
* http://www.keithschwarz.com/darts-dice-coins
*
* Based on C implementation:
* https://jugit.fz-juelich.de/mlz/ransampl
*
* Ported from https://github.com/BlockchainCommons/URKit
*/
public class RandomSampler {
/* The probability and alias tables. */
private final double[] probs;
private final int[] aliases;
public RandomSampler(List<Double> probabilities) {
if(probabilities.stream().anyMatch(prob -> prob < 0)) {
throw new IllegalArgumentException("Probabilties must be > 0");
}
// Normalize given probabilities
double sum = probabilities.stream().reduce(0d, Double::sum);
int n = probabilities.size();
List<Double> P = probabilities.stream().map(prob -> prob * (double)n / sum).collect(Collectors.toList());
List<Integer> S = new ArrayList<>();
List<Integer> L = new ArrayList<>();
// Set separate index lists for small and large probabilities:
for(int i = n - 1; i >= 0; i--) {
// at variance from Schwarz, we reverse the index order
if(P.get(i) < 1d) {
S.add(i);
} else {
L.add(i);
}
}
// Work through index lists
double[] probs = new double[n];
int[] aliases = new int[n];
while(!S.isEmpty() && !L.isEmpty()) {
int a = S.remove(S.size() - 1);
int g = L.remove(L.size() - 1);
probs[a] = P.get(a);
aliases[a] = g;
P.set(g, P.get(g) + P.get(a) - 1);
if(P.get(g) < 1) {
S.add(g);
} else {
L.add(g);
}
}
while(!L.isEmpty()) {
probs[L.remove(L.size() - 1)] = 1;
}
while(!S.isEmpty()) {
// can only happen through numeric instability
probs[S.remove(S.size() - 1)] = 1;
}
this.probs = probs;
this.aliases = aliases;
}
public int next(Random random) {
double r1 = random.nextDouble();
double r2 = random.nextDouble();
int n = probs.length;
int i = (int)((double)n * r1);
return r2 < probs[i] ? i : aliases[i];
}
}

View file

@ -3,13 +3,11 @@ 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;
@ -37,17 +35,17 @@ public class URTest {
"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/10-9/ltbkascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtwdkiplzs",
"ur:bytes/11-9/ltbdascfadaxcywenbpljkhdcahelbknlkuejnbadmssfhfrdpsbiegecpasvssovlgeykssjykklronvsjkvetiiapk",
"ur:bytes/12-9/ltbnascfadaxcywenbpljkhdcarllaluzodmgstospeyiefmwejlwtpedamktksrvlcygmzmmovovllarodtmtbnptrs",
"ur:bytes/13-9/ltbtascfadaxcywenbpljkhdcamtkgtpknghchchyketwsvwgwfdhpgmgtylctotztpdrpayoschcmhplffziachrfgd",
"ur:bytes/14-9/ltbaascfadaxcywenbpljkhdcapazmwnvonnvdnsbyleynwtnsjkjndeoldydkbkdslgjkbbkortbelomueekgvstegt",
"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"
"ur:bytes/16-9/ltbeascfadaxcywenbpljkhdcawygekobamwtlihsnpalpsghenskkiynthdzttsimtojetprsttmukirlrsbtamjtpd",
"ur:bytes/17-9/ltbyascfadaxcywenbpljkhdcamklgftaxykpewyrtqzhydntpnytyisincxmhtbceaykolduortotiaiaiafhiaoyce",
"ur:bytes/18-9/ltbgascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtntwkbkwy",
"ur:bytes/19-9/ltbwascfadaxcywenbpljkhdcadekicpaajootjzpsdrbalteywllbdsnbinaerkurspbncxgslgftvtsrjtksplcpeo",
"ur:bytes/20-9/ltbbascfadaxcywenbpljkhdcayapmrleeleaxpasfrtrdkncffwjyjzgyetdmlewtkpktgllepfrltatazcksmhkbot"
};
Assert.assertArrayEquals("", expectedParts, parts.toArray());
}
@ -70,37 +68,6 @@ public class URTest {
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];

View file

@ -11,6 +11,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.zip.CRC32;
public class FountainCodesTest {
@Test
@ -25,9 +26,9 @@ public class FountainCodesTest {
@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};
RandomSampler randomSampler = new RandomSampler(List.of(1d, 2d, 4d, 8d));
int[] numbers = IntStream.range(0, 500).map(i -> randomSampler.next(rng)).toArray();
int[] expectedNumbers = new int[] {3, 3, 3, 3, 3, 3, 3, 0, 2, 3, 3, 3, 3, 1, 2, 2, 1, 3, 3, 2, 3, 3, 1, 1, 2, 1, 1, 3, 1, 3, 1, 2, 0, 2, 1, 0, 3, 3, 3, 1, 3, 3, 3, 3, 1, 3, 2, 3, 2, 2, 3, 3, 3, 3, 2, 3, 3, 0, 3, 3, 3, 3, 1, 2, 3, 3, 2, 2, 2, 1, 2, 2, 1, 2, 3, 1, 3, 0, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 1, 3, 3, 2, 0, 2, 2, 3, 1, 1, 2, 3, 2, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 2, 3, 1, 2, 1, 1, 3, 1, 3, 2, 2, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 2, 3, 3, 1, 2, 3, 3, 1, 3, 2, 3, 3, 3, 2, 3, 1, 3, 0, 3, 2, 1, 1, 3, 1, 3, 2, 3, 3, 3, 3, 2, 0, 3, 3, 1, 3, 0, 2, 1, 3, 3, 1, 1, 3, 1, 2, 3, 3, 3, 0, 2, 3, 2, 0, 1, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 2, 3, 3, 2, 0, 2, 3, 3, 3, 3, 2, 1, 1, 1, 2, 1, 3, 3, 3, 2, 2, 3, 3, 1, 2, 3, 0, 3, 2, 3, 3, 3, 3, 0, 2, 2, 3, 2, 2, 3, 3, 3, 3, 1, 3, 2, 3, 3, 3, 3, 3, 2, 2, 3, 1, 3, 0, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 2, 2, 2, 3, 1, 1, 3, 2, 2, 0, 3, 2, 1, 2, 1, 0, 3, 3, 3, 2, 2, 3, 2, 1, 2, 0, 0, 3, 3, 2, 3, 3, 2, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 1, 1, 3, 2, 2, 3, 1, 1, 0, 1, 3, 2, 3, 3, 2, 3, 3, 2, 3, 3, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 1, 2, 3, 3, 2, 2, 2, 2, 3, 3, 2, 0, 2, 1, 3, 3, 3, 3, 0, 3, 3, 3, 3, 2, 2, 3, 1, 3, 3, 3, 2, 3, 3, 3, 2, 3, 3, 3, 3, 2, 3, 2, 1, 3, 3, 3, 3, 2, 2, 0, 1, 2, 3, 2, 0, 3, 3, 3, 3, 3, 3, 1, 3, 3, 2, 3, 2, 2, 3, 3, 3, 3, 3, 2, 2, 3, 3, 2, 2, 2, 1, 3, 3, 3, 3, 1, 2, 3, 2, 3, 3, 2, 3, 2, 3, 3, 3, 2, 3, 1, 2, 3, 2, 1, 1, 3, 3, 2, 3, 3, 2, 3, 3, 0, 0, 1, 3, 3, 2, 3, 3, 3, 3, 1, 3, 3, 0, 3, 2, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 0, 3, 3, 2};
Assert.assertArrayEquals(expectedNumbers, numbers);
}
@ -42,6 +43,96 @@ public class FountainCodesTest {
Assert.assertEquals(expectedNumbers, numbers);
}
@Test
public void testPartitionAndJoin() {
byte[] message = URTest.makeMessage(1024, "Wolf");
int fragmentLen = FountainEncoder.findNominalFragmentLength(message.length, 10, 100);
List<byte[]> fragments = FountainEncoder.partitionMessage(message, fragmentLen);
List<String> fragmentsHex = fragments.stream().map(Utils::bytesToHex).collect(Collectors.toList());
String[] expectedFragmentsHex = new String[] {
"916ec65cf77cadf55cd7f9cda1a1030026ddd42e905b77adc36e4f2d3ccba44f7f04f2de44f42d84c374a0e149136f25b01852545961d55f7f7a8cde6d0e2ec43f3b2dcb644a2209e8c9e34af5c4747984a5e873c9cf5f965e25ee29039f",
"df8ca74f1c769fc07eb7ebaec46e0695aea6cbd60b3ec4bbff1b9ffe8a9e7240129377b9d3711ed38d412fbb4442256f1e6f595e0fc57fed451fb0a0101fb76b1fb1e1b88cfdfdaa946294a47de8fff173f021c0e6f65b05c0a494e50791",
"270a0050a73ae69b6725505a2ec8a5791457c9876dd34aadd192a53aa0dc66b556c0c215c7ceb8248b717c22951e65305b56a3706e3e86eb01c803bbf915d80edcd64d4d41977fa6f78dc07eecd072aae5bc8a852397e06034dba6a0b570",
"797c3a89b16673c94838d884923b8186ee2db5c98407cab15e13678d072b43e406ad49477c2e45e85e52ca82a94f6df7bbbe7afbed3a3a830029f29090f25217e48d1f42993a640a67916aa7480177354cc7440215ae41e4d02eae9a1912",
"33a6d4922a792c1b7244aa879fefdb4628dc8b0923568869a983b8c661ffab9b2ed2c149e38d41fba090b94155adbed32f8b18142ff0d7de4eeef2b04adf26f2456b46775c6c20b37602df7da179e2332feba8329bbb8d727a138b4ba7a5",
"03215eda2ef1e953d89383a382c11d3f2cad37a4ee59a91236a3e56dcf89f6ac81dd4159989c317bd649d9cbc617f73fe10033bd288c60977481a09b343d3f676070e67da757b86de27bfca74392bac2996f7822a7d8f71a489ec6180390",
"089ea80a8fcd6526413ec6c9a339115f111d78ef21d456660aa85f790910ffa2dc58d6a5b93705caef1091474938bd312427021ad1eeafbd19e0d916ddb111fabd8dcab5ad6a6ec3a9c6973809580cb2c164e26686b5b98cfb017a337968",
"c7daaa14ae5152a067277b1b3902677d979f8e39cc2aafb3bc06fcf69160a853e6869dcc09a11b5009f91e6b89e5b927ab1527a735660faa6012b420dd926d940d742be6a64fb01cdc0cff9faa323f02ba41436871a0eab851e7f5782d10",
"fbefde2a7e9ae9dc1e5c2c48f74f6c824ce9ef3c89f68800d44587bedc4ab417cfb3e7447d90e1e417e6e05d30e87239d3a5d1d45993d4461e60a0192831640aa32dedde185a371ded2ae15f8a93dba8809482ce49225daadfbb0fec629e",
"23880789bdf9ed73be57fa84d555134630e8d0f7df48349f29869a477c13ccca9cd555ac42ad7f568416c3d61959d0ed568b2b81c7771e9088ad7fd55fd4386bafbf5a528c30f107139249357368ffa980de2c76ddd9ce4191376be0e6b5",
"170010067e2e75ebe2d2904aeb1f89d5dc98cd4a6f2faaa8be6d03354c990fd895a97feb54668473e9d942bb99e196d897e8f1b01625cf48a7b78d249bb4985c065aa8cd1402ed2ba1b6f908f63dcd84b66425df00000000000000000000"
};
Assert.assertEquals(Arrays.asList(expectedFragmentsHex), fragmentsHex);
byte[] rejoinedMessage = FountainDecoder.joinFragments(fragments, message.length);
Assert.assertArrayEquals(message, rejoinedMessage);
}
@Test
public void testChooseDegree() {
byte[] message = URTest.makeMessage(1024, "Wolf");
int fragmentLen = FountainEncoder.findNominalFragmentLength(message.length, 10, 100);
List<byte[]> fragments = FountainEncoder.partitionMessage(message, fragmentLen);
List<Integer> degrees = IntStream.rangeClosed(1, 200).mapToObj( nonce -> {
RandomXoshiro256StarStar partRng = new RandomXoshiro256StarStar("Wolf-" + nonce);
return FountainUtils.chooseDegree(fragments.size(), partRng);
}).collect(Collectors.toList());
Integer[] expectedDegrees = new Integer[] {
11, 3, 6, 5, 2, 1, 2, 11, 1, 3, 9, 10, 10, 4, 2, 1, 1, 2, 1, 1, 5, 2, 4, 10, 3, 2, 1, 1, 3, 11, 2, 6, 2, 9, 9, 2, 6, 7, 2, 5, 2, 4, 3, 1, 6, 11, 2, 11, 3, 1, 6, 3, 1, 4, 5, 3, 6, 1, 1, 3, 1, 2, 2, 1, 4, 5, 1, 1, 9, 1, 1, 6, 4, 1, 5, 1, 2, 2, 3, 1, 1, 5, 2, 6, 1, 7, 11, 1, 8, 1, 5, 1, 1, 2, 2, 6, 4, 10, 1, 2, 5, 5, 5, 1, 1, 4, 1, 1, 1, 3, 5, 5, 5, 1, 4, 3, 3, 5, 1, 11, 3, 2, 8, 1, 2, 1, 1, 4, 5, 2, 1, 1, 1, 5, 6, 11, 10, 7, 4, 7, 1, 5, 3, 1, 1, 9, 1, 2, 5, 5, 2, 2, 3, 10, 1, 3, 2, 3, 3, 1, 1, 2, 1, 3, 2, 2, 1, 3, 8, 4, 1, 11, 6, 3, 1, 1, 1, 1, 1, 3, 1, 2, 1, 10, 1, 1, 8, 2, 7, 1, 2, 1, 9, 2, 10, 2, 1, 3, 4, 10
};
Assert.assertEquals(Arrays.asList(expectedDegrees), degrees);
}
@Test
public void testChooseFragment() {
byte[] message = URTest.makeMessage(1024, "Wolf");
CRC32 crc32 = new CRC32();
crc32.update(message);
long checksum = crc32.getValue();
int fragmentLen = FountainEncoder.findNominalFragmentLength(message.length, 10, 100);
List<byte[]> fragments = FountainEncoder.partitionMessage(message, fragmentLen);
List<List<Integer>> partIndexes = IntStream.rangeClosed(1, 30).mapToObj(nonce -> {
return FountainUtils.chooseFragments(nonce, fragments.size(), checksum).stream().sorted().collect(Collectors.toList());
}).collect(Collectors.toList());
Integer[][] expectedFragmentIndexes = new Integer[][] {
{0},
{1},
{2},
{3},
{4},
{5},
{6},
{7},
{8},
{9},
{10},
{9},
{2, 5, 6, 8, 9, 10},
{8},
{1, 5},
{1},
{0, 2, 4, 5, 8, 10},
{5},
{2},
{2},
{0, 1, 3, 4, 5, 7, 9, 10},
{0, 1, 2, 3, 5, 6, 8, 9, 10},
{0, 2, 4, 5, 7, 8, 9, 10},
{3, 5},
{4},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
{0, 1, 3, 4, 5, 6, 7, 9, 10},
{6},
{5, 6},
{7}
};
List<List<Integer>> expectedPartIndexes = Arrays.stream(expectedFragmentIndexes).map(Arrays::asList).collect(Collectors.toList());
Assert.assertEquals(expectedPartIndexes, partIndexes);
}
@Test
public void testXOR() {
RandomXoshiro256StarStar rng = new RandomXoshiro256StarStar("Wolf");
@ -73,17 +164,17 @@ public class FountainCodesTest {
"8507091901001a0167aa07581d73f021c0e6f65b05c0a494e50791270a0050a73ae69b6725505a2ec8a5",
"8508091901001a0167aa07581d791457c9876dd34aadd192a53aa0dc66b556c0c215c7ceb8248b717c22",
"8509091901001a0167aa07581d951e65305b56a3706e3e86eb01c803bbf915d80edcd64d4d0000000000",
"850a091901001a0167aa07581d4a1b58fa2733399e5ee04d87a2d1628186e3cd250f3ae0e25d7ae7a22b",
"850b091901001a0167aa07581dd35acd70953cf29b542a94cbd75790c73cb4cb1056d56557bf0b70b936",
"850c091901001a0167aa07581d8cde6d0e2ec43f3b2dcb644a2209e8c9e34af5c4747984a5e873c9cf5f",
"850a091901001a0167aa07581d330f0f33a05eead4f331df229871bee733b50de71afd2e5a79f196de09",
"850b091901001a0167aa07581d3b205ce5e52d8c24a52cffa34c564fa1af3fdffcd349dc4258ee4ee828",
"850c091901001a0167aa07581ddd7bf725ea6c16d531b5f03254783803048ca08b87148daacd1cd7a006",
"850d091901001a0167aa07581d760be7ad1c6187902bbc04f539b9ee5eb8ea6833222edea36031306c01",
"850e091901001a0167aa07581dcba44f7f04f2de44f42d84c374a0e149136f25b01852545961d55f7f7a",
"850f091901001a0167aa07581d262518878e747c6eee337fbbd189f77b385efe55597b54cab65b7f8ac0",
"8510091901001a0167aa07581d2d4a0b8fb95226315ab796cd72f9c5f8ea2a5a84221f7e31318f71b7df",
"8511091901001a0167aa07581d81bf178523c1e7daaacdc944d67183c8958ce8951768b4bb3cafb8dd51",
"8512091901001a0167aa07581d8e1548d10a2cd18416a3428bcb1fe2e3cac640274e91df20bdbd4e4df9",
"8513091901001a0167aa07581d8a65ebbda606df01244a3dad6b76e258150e4c07021ce5c07ed30160c5",
"8514091901001a0167aa07581d2b44620c8371a48f6935d2b525f19c7a4e98e3043a6a64d462870e98ce"
"850e091901001a0167aa07581d5bf4031217d2c3254b088fa7553778b5003632f46e21db129416f65b55",
"850f091901001a0167aa07581d73f021c0e6f65b05c0a494e50791270a0050a73ae69b6725505a2ec8a5",
"8510091901001a0167aa07581db8546ebfe2048541348910267331c643133f828afec9337c318f71b7df",
"8511091901001a0167aa07581d23dedeea74e3a0fb052befabefa13e2f80e4315c9dceed4c8630612e64",
"8512091901001a0167aa07581dd01a8daee769ce34b6b35d3ca0005302724abddae405bdb419c0a6b208",
"8513091901001a0167aa07581d3171c5dc365766eff25ae47c6f10e7de48cfb8474e050e5fe997a6dc24",
"8514091901001a0167aa07581de055c2433562184fa71b4be94f262e200f01c6f74c284b0dc6fae6673f"
};
Assert.assertEquals(Arrays.asList(expectedPartsHex), partsHex);