diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java new file mode 100644 index 00000000..328b1e25 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java @@ -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 { + 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 { + @Override + protected Task createTask() { + return new Task<>() { + protected Boolean call() throws ImportException { + Image qrImage = getQrCode(currentPart); + qrImageView.setImage(qrImage); + nextPart(); + + return true; + } + }; + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 5d19a4ed..815c60a9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -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) { diff --git a/src/main/java/com/sparrowwallet/sparrow/ur/UR.java b/src/main/java/com/sparrowwallet/sparrow/ur/UR.java index 830b04b9..d2198ce3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/ur/UR.java +++ b/src/main/java/com/sparrowwallet/sparrow/ur/UR.java @@ -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) { diff --git a/src/main/java/com/sparrowwallet/sparrow/ur/fountain/AliasMethod.java b/src/main/java/com/sparrowwallet/sparrow/ur/fountain/AliasMethod.java deleted file mode 100644 index 13f89cb4..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/ur/fountain/AliasMethod.java +++ /dev/null @@ -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. - *

- * 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 probabilities) { - this(probabilities, new Random()); - } - - /** - * Constructs a new AliasMethod to sample from a discrete distribution and - * hand back outcomes based on the probability distribution. - *

- * 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 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(probabilities); - - /* Create two stacks to act as worklists as we populate the tables. */ - Deque small = new ArrayDeque(); - Deque large = new ArrayDeque(); - - /* 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]; - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/ur/fountain/FountainUtils.java b/src/main/java/com/sparrowwallet/sparrow/ur/fountain/FountainUtils.java index 56673957..9235fdc3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/ur/fountain/FountainUtils.java +++ b/src/main/java/com/sparrowwallet/sparrow/ur/fountain/FountainUtils.java @@ -31,8 +31,8 @@ public class FountainUtils { static int chooseDegree(int seqLen, RandomXoshiro256StarStar rng) { List 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 shuffled(List indexes, RandomXoshiro256StarStar rng) { diff --git a/src/main/java/com/sparrowwallet/sparrow/ur/fountain/RandomSampler.java b/src/main/java/com/sparrowwallet/sparrow/ur/fountain/RandomSampler.java new file mode 100644 index 00000000..13d1ba24 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/ur/fountain/RandomSampler.java @@ -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 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 P = probabilities.stream().map(prob -> prob * (double)n / sum).collect(Collectors.toList()); + + List S = new ArrayList<>(); + List 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]; + } +} diff --git a/src/test/java/com/sparrowwallet/sparrow/ur/URTest.java b/src/test/java/com/sparrowwallet/sparrow/ur/URTest.java index 693f17b7..98a0cb95 100644 --- a/src/test/java/com/sparrowwallet/sparrow/ur/URTest.java +++ b/src/test/java/com/sparrowwallet/sparrow/ur/URTest.java @@ -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 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]; diff --git a/src/test/java/com/sparrowwallet/sparrow/ur/fountain/FountainCodesTest.java b/src/test/java/com/sparrowwallet/sparrow/ur/fountain/FountainCodesTest.java index 27446fe2..e140c226 100644 --- a/src/test/java/com/sparrowwallet/sparrow/ur/fountain/FountainCodesTest.java +++ b/src/test/java/com/sparrowwallet/sparrow/ur/fountain/FountainCodesTest.java @@ -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 fragments = FountainEncoder.partitionMessage(message, fragmentLen); + List 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 fragments = FountainEncoder.partitionMessage(message, fragmentLen); + List 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 fragments = FountainEncoder.partitionMessage(message, fragmentLen); + List> 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> 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);