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