mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-09 08:14:43 +00:00
break out UR implementation into hummingbird project
This commit is contained in:
@@ -7,8 +7,8 @@ import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
import com.sparrowwallet.sparrow.AppController;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.ur.UR;
|
||||
import com.sparrowwallet.sparrow.ur.UREncoder;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.UREncoder;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
@@ -39,8 +39,8 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
|
||||
private String currentPart;
|
||||
|
||||
public QRDisplayDialog(byte[] data) {
|
||||
this(UR.fromBytes(data));
|
||||
public QRDisplayDialog(String type, byte[] data) throws UR.URException {
|
||||
this(UR.fromBytes(type, data));
|
||||
}
|
||||
|
||||
public QRDisplayDialog(UR ur) {
|
||||
|
||||
@@ -9,9 +9,9 @@ import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.sparrow.AppController;
|
||||
import com.sparrowwallet.sparrow.ur.ResultType;
|
||||
import com.sparrowwallet.sparrow.ur.UR;
|
||||
import com.sparrowwallet.sparrow.ur.URDecoder;
|
||||
import com.sparrowwallet.hummingbird.ResultType;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.URDecoder;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.sparrow.AppController;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
@@ -589,8 +590,12 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
ToggleButton toggleButton = (ToggleButton)event.getSource();
|
||||
toggleButton.setSelected(false);
|
||||
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(headersForm.getPsbt().serialize());
|
||||
qrDisplayDialog.show();
|
||||
try {
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(UR.CRYPTO_PSBT_TYPE, headersForm.getPsbt().serialize());
|
||||
qrDisplayDialog.show();
|
||||
} catch(UR.URException e) {
|
||||
log.error("Error creating PSBT UR", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void scanPSBT(ActionEvent event) {
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Ported from https://github.com/BlockchainCommons/URKit
|
||||
*/
|
||||
public class Bytewords {
|
||||
public static final String BYTEWORDS = "ableacidalsoapexaquaarchatomauntawayaxisbackbaldbarnbeltbetabiasbluebodybragbrewbulbbuzzcalmcashcatschefcityclawcodecolacookcostcruxcurlcuspcyandarkdatadaysdelidicedietdoordowndrawdropdrumdulldutyeacheasyechoedgeepicevenexamexiteyesfactfairfernfigsfilmfishfizzflapflewfluxfoxyfreefrogfuelfundgalagamegeargemsgiftgirlglowgoodgraygrimgurugushgyrohalfhanghardhawkheathelphighhillholyhopehornhutsicedideaidleinchinkyintoirisironitemjadejazzjoinjoltjowljudojugsjumpjunkjurykeepkenokeptkeyskickkilnkingkitekiwiknoblamblavalazyleaflegsliarlimplionlistlogoloudloveluaulucklungmainmanymathmazememomenumeowmildmintmissmonknailnavyneednewsnextnoonnotenumbobeyoboeomitonyxopenovalowlspaidpartpeckplaypluspoempoolposepuffpumapurrquadquizraceramprealredorichroadrockroofrubyruinrunsrustsafesagascarsetssilkskewslotsoapsolosongstubsurfswantacotasktaxitenttiedtimetinytoiltombtoystriptunatwinuglyundouniturgeuservastveryvetovialvibeviewvisavoidvowswallwandwarmwaspwavewaxywebswhatwhenwhizwolfworkyankyawnyellyogayurtzapszerozestzinczonezoom";
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.ur;
|
||||
|
||||
public enum ResultType {
|
||||
SUCCESS, FAILURE;
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.ur;
|
||||
|
||||
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.ByteString;
|
||||
import co.nstant.in.cbor.model.DataItem;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ported from https://github.com/BlockchainCommons/URKit
|
||||
*/
|
||||
public class UR {
|
||||
public static final String UR_PREFIX = "ur";
|
||||
public static final String BYTES_TYPE = "bytes";
|
||||
public static final String CRYPTO_PSBT_TYPE = "crypto-psbt";
|
||||
|
||||
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 byte[] toBytes() throws InvalidTypeException, CborException {
|
||||
if(!BYTES_TYPE.equals(getType())) {
|
||||
throw new InvalidTypeException("Not a " + BYTES_TYPE + " type");
|
||||
}
|
||||
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(getCbor());
|
||||
List<DataItem> dataItems = new CborDecoder(bais).decode();
|
||||
return ((ByteString)dataItems.get(0)).getBytes();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public static UR fromBytes(byte[] data) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
new CborEncoder(baos).encode(new CborBuilder()
|
||||
.add(data)
|
||||
.build());
|
||||
byte[] cbor = baos.toByteArray();
|
||||
|
||||
return new UR("bytes", cbor);
|
||||
} catch(InvalidTypeException | CborException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return UREncoder.encode(this);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Ported from https://github.com/BlockchainCommons/URKit
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.ur;
|
||||
|
||||
import com.sparrowwallet.sparrow.ur.fountain.FountainEncoder;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
/**
|
||||
* Ported from https://github.com/BlockchainCommons/URKit
|
||||
*/
|
||||
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.UR_PREFIX, 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);
|
||||
}
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.ur.fountain;
|
||||
|
||||
import com.sparrowwallet.sparrow.ur.ResultType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Ported from https://github.com/BlockchainCommons/URKit
|
||||
*/
|
||||
public class FountainDecoder {
|
||||
private static final Logger log = LoggerFactory.getLogger(FountainDecoder.class);
|
||||
|
||||
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);
|
||||
log.debug("processed: " + processedPartsCount + " expected: " + getExpectedPartCount() + " received: " + recievedPartIndexes.size() + " percent: " + percent + "%");
|
||||
}
|
||||
|
||||
private void printPart(Part part) {
|
||||
List<Integer> sorted = part.partIndexes.stream().sorted().collect(Collectors.toList());
|
||||
log.debug("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());
|
||||
|
||||
log.debug("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;
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Ported from https://github.com/BlockchainCommons/URKit
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Ported from https://github.com/BlockchainCommons/URKit
|
||||
*/
|
||||
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());
|
||||
RandomSampler randomSampler = new RandomSampler(degreeProbabilties);
|
||||
return randomSampler.next(rng) + 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;
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
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];
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
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 <vigna@acm.org> (original C code)
|
||||
* @author Una Thompson <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;
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,9 @@ open module com.sparrowwallet.sparrow {
|
||||
requires simple.json.rpc.core;
|
||||
requires org.jetbrains.annotations;
|
||||
requires com.fasterxml.jackson.databind;
|
||||
requires cbor;
|
||||
requires webcam.capture;
|
||||
requires netlayer.jpms;
|
||||
requires hummingbird;
|
||||
requires centerdevice.nsmenufx;
|
||||
requires jcommander;
|
||||
requires slf4j.api;
|
||||
|
||||
Reference in New Issue
Block a user