mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-18 02:07:43 +00:00
1600 lines
50 KiB
Dart
1600 lines
50 KiB
Dart
import 'socketwrapper.dart';
|
||
import 'package:protobuf/protobuf.dart';
|
||
import 'dart:convert';
|
||
import 'dart:math';
|
||
import 'fusion.pb.dart';
|
||
import 'util.dart';
|
||
import 'dart:typed_data';
|
||
import 'package:fixnum/fixnum.dart';
|
||
import 'pedersen.dart';
|
||
import 'dart:io';
|
||
import 'package:crypto/crypto.dart';
|
||
import 'dart:async';
|
||
import 'comms.dart';
|
||
import 'protocol.dart';
|
||
import 'package:fixnum/fixnum.dart'; // so int and intt64 can be combined in some protobuff code
|
||
import 'encrypt.dart';
|
||
import "package:pointycastle/export.dart";
|
||
import 'covert.dart';
|
||
import 'connection.dart';
|
||
import 'package:collection/collection.dart';
|
||
import 'package:convert/convert.dart';
|
||
import 'validation.dart';
|
||
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
|
||
|
||
|
||
class FusionError implements Exception {
|
||
final String message;
|
||
FusionError(this.message);
|
||
String toString() => "FusionError: $message";
|
||
}
|
||
|
||
class ComponentResult {
|
||
final Uint8List commitment;
|
||
final int counter;
|
||
final Uint8List component;
|
||
final Proof proof;
|
||
final Uint8List privateKey;
|
||
final dynamic pedersenAmount; // replace dynamic with actual type
|
||
final dynamic pedersenNonce; // replace dynamic with actual type
|
||
|
||
ComponentResult(
|
||
this.commitment,
|
||
this.counter,
|
||
this.component,
|
||
this.proof,
|
||
this.privateKey,
|
||
{this.pedersenAmount,
|
||
this.pedersenNonce}
|
||
);
|
||
}
|
||
|
||
class Transaction {
|
||
List<Input> Inputs = [];
|
||
List<Output> Outputs = [];
|
||
|
||
Transaction();
|
||
|
||
|
||
static Tuple txFromComponents(List<dynamic> allComponents, List<dynamic> sessionHash) {
|
||
Transaction tx = Transaction(); // Initialize a new Transaction
|
||
// This should be based on wallet layer... implement the logic of constructing the transaction from components
|
||
// For now, it just initializes Inputs and Outputs as empty lists
|
||
tx.Inputs = [];
|
||
tx.Outputs = [];
|
||
|
||
// For now, just returning an empty list for inputIndices
|
||
List<int> inputIndices = [];
|
||
|
||
return Tuple(tx, inputIndices);
|
||
}
|
||
|
||
List<int> serializePreimage(int index, int hashType, {bool useCache = true}) {
|
||
// Add implementation here
|
||
// For now, returning an empty byte array
|
||
return [];
|
||
}
|
||
|
||
String serialize() {
|
||
// To implement...
|
||
return "";
|
||
}
|
||
|
||
bool isComplete() {
|
||
// implement based on wallet.
|
||
return true;
|
||
}
|
||
|
||
String txid() {
|
||
// To implement...
|
||
return "";
|
||
}
|
||
}
|
||
|
||
|
||
class Input {
|
||
List<int> prevTxid;
|
||
int prevIndex;
|
||
List<int> pubKey;
|
||
int amount;
|
||
List<dynamic> signatures =[];
|
||
|
||
Input({required this.prevTxid, required this.prevIndex, required this.pubKey, required this.amount});
|
||
|
||
int sizeOfInput() {
|
||
assert(1 < pubKey.length && pubKey.length < 76); // need to assume regular push opcode
|
||
return 108 + pubKey.length;
|
||
}
|
||
|
||
int get value {
|
||
return amount;
|
||
}
|
||
|
||
String getPubKey(int pubkey_index) {
|
||
// TO BE IMPLEMENTED...
|
||
return "";
|
||
}
|
||
|
||
String getPrivKey(int pubkey_index) {
|
||
// TO BE IMPLEMENTED...
|
||
return "";
|
||
}
|
||
|
||
|
||
static Input fromInputComponent(InputComponent inputComponent) {
|
||
return Input(
|
||
prevTxid: inputComponent.prevTxid, // Make sure the types are matching
|
||
prevIndex: inputComponent.prevIndex.toInt(),
|
||
pubKey: inputComponent.pubkey,
|
||
amount: inputComponent.amount.toInt(),
|
||
);
|
||
}
|
||
|
||
static Input fromStackUTXO(UTXO utxo) {
|
||
return Input(
|
||
prevTxid: utf8.encode(utxo.txid), // Convert txid to a List<int>
|
||
prevIndex: utxo.vout,
|
||
pubKey: utf8.encode('0000'), // Placeholder
|
||
amount: utxo.value,
|
||
);
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
class Output {
|
||
int value;
|
||
Address addr;
|
||
|
||
int amount=0;
|
||
|
||
Output({required this.value, required this.addr});
|
||
|
||
int sizeOfOutput() {
|
||
List<int> scriptpubkey = addr.toScript(); // assuming addr.toScript() returns List<int> that represents the scriptpubkey
|
||
assert(scriptpubkey.length < 253);
|
||
return 9 + scriptpubkey.length;
|
||
}
|
||
|
||
static Output fromOutputComponent(OutputComponent outputComponent) {
|
||
Address address = Address.fromScriptPubKey(outputComponent.scriptpubkey);
|
||
return Output(
|
||
value: outputComponent.amount.toInt(),
|
||
addr: address,
|
||
);
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
// Class to handle fusion
|
||
class Fusion {
|
||
|
||
List<Input> coins = []; //"coins" and "inputs" are often synonmous in the original python code.
|
||
List<Output> outputs =[];
|
||
bool server_connected_and_greeted = false;
|
||
bool stopping = false;
|
||
bool stopping_if_not_running = false;
|
||
String stopReason="";
|
||
String tor_host="";
|
||
bool server_ssl= false;
|
||
String server_host ="cashfusion.stackwallet.com";
|
||
int server_port = 8787;
|
||
|
||
//String server_host = "fusion.servo.cash";
|
||
//int server_port = 8789;
|
||
|
||
int tor_port = 0;
|
||
int roundcount = 0;
|
||
String txid="";
|
||
|
||
Tuple<String, String> status = Tuple("", "");
|
||
Connection? connection;
|
||
|
||
int numComponents =0;
|
||
double componentFeeRate=0;
|
||
double minExcessFee=0;
|
||
double maxExcessFee=0;
|
||
List<int> availableTiers =[];
|
||
|
||
int maxOutputs=0;
|
||
int safety_sum_in =0;
|
||
Map<int, int> safety_exess_fees = {};
|
||
Map<int, List<int>> tierOutputs ={}; // not sure if this should be using outputs class.
|
||
|
||
int inactiveTimeLimit = 600000; // this is in ms... equates to 10 minutes.
|
||
int tier = 0;
|
||
int covertPort = 0;
|
||
bool covertSSL = false;
|
||
double beginTime = 0.0; // represent time in seconds.
|
||
List<int> lastHash = <int>[];
|
||
List<Address> reservedAddresses = <Address>[];
|
||
int safetyExcessFee = 0;
|
||
DateTime t_fusionBegin = DateTime.now();
|
||
Uint8List covertDomainB = Uint8List(0);
|
||
|
||
var txInputIndices;
|
||
Transaction tx= Transaction();
|
||
List<int> myComponentIndexes=[];
|
||
List<int> myCommitmentIndexes=[];
|
||
Set<int> badComponents ={};
|
||
|
||
Fusion () {
|
||
//initializeConnection(host, port)
|
||
}
|
||
/*
|
||
Future<void> initializeConnection(String host, int port) async {
|
||
Socket socket = await Socket.connect(host, port);
|
||
connection = Connection()..socket = socket;
|
||
}
|
||
*/
|
||
|
||
Future<void> add_coins_from_wallet(List<UTXO> utxoList) async {
|
||
// Convert each UTXO to an Input and add to 'coins'
|
||
for (UTXO utxo in utxoList) {
|
||
coins.add(Input.fromStackUTXO(utxo));
|
||
}
|
||
}
|
||
|
||
Future<void> fusion_run() async {
|
||
|
||
print ("DEBUG FUSION 223...fusion run....");
|
||
try {
|
||
|
||
try {
|
||
|
||
// Check compatibility - This was done in python version to see if fast libsec installed.
|
||
// For now , in dart, just pass this test.
|
||
;
|
||
} on Exception catch(e) {
|
||
// handle exception, rethrow as a custom FusionError
|
||
throw FusionError("Incompatible: " + e.toString());
|
||
}
|
||
|
||
// Check if can connect to Tor proxy, if not, raise FusionError. Empty String treated as no host.
|
||
if (tor_host.isNotEmpty && tor_port != 0 && !await isTorPort(tor_host, tor_port)) {
|
||
throw FusionError("Can't connect to Tor proxy at $tor_host:$tor_port");
|
||
}
|
||
|
||
try {
|
||
// Check stop condition
|
||
check_stop(running: false);
|
||
}
|
||
catch (e) {
|
||
print (e);
|
||
}
|
||
|
||
try {
|
||
// Check coins
|
||
check_coins();
|
||
}
|
||
catch (e) {
|
||
print (e);
|
||
}
|
||
|
||
|
||
|
||
// Connect to server
|
||
status = Tuple("connecting", "");
|
||
try {
|
||
connection = await openConnection(server_host, server_port, connTimeout: 5.0, defaultTimeout: 5.0, ssl: server_ssl);
|
||
} catch (e) {
|
||
print("Connect failed: $e");
|
||
String sslstr = server_ssl ? ' SSL ' : '';
|
||
throw FusionError('Could not connect to $sslstr$server_host:$server_port');
|
||
}
|
||
|
||
|
||
// Once connection is successful, wrap operations inside this block
|
||
// Within this block, version checks, downloads server params, handles coins and runs rounds
|
||
try {
|
||
|
||
|
||
SocketWrapper socketwrapper = SocketWrapper(server_host, server_port);
|
||
await socketwrapper.connect();
|
||
|
||
// Version check and download server params.
|
||
await greet(socketwrapper);
|
||
|
||
socketwrapper.status();
|
||
server_connected_and_greeted = true;
|
||
notify_server_status(true);
|
||
|
||
// In principle we can hook a pause in here -- user can insert coins after seeing server params.
|
||
|
||
try {
|
||
if (coins.isEmpty) {
|
||
throw FusionError('Started with no coins');
|
||
return;
|
||
}
|
||
}
|
||
catch (e) {
|
||
print(e);
|
||
return;
|
||
}
|
||
|
||
await allocateOutputs(socketwrapper);
|
||
// In principle we can hook a pause in here -- user can tweak tier_outputs, perhaps cancelling some unwanted tiers.
|
||
|
||
// Register for tiers, wait for a pool.
|
||
await registerAndWait(socketwrapper);
|
||
|
||
|
||
// launch the covert submitter
|
||
CovertSubmitter covert = await startCovert();
|
||
try {
|
||
// Pool started. Keep running rounds until fail or complete.
|
||
while (true) {
|
||
roundcount += 1;
|
||
if (await runRound(covert)) {
|
||
break;
|
||
}
|
||
}
|
||
} finally {
|
||
covert.stop();
|
||
}
|
||
} finally {
|
||
(await connection)?.close();
|
||
}
|
||
|
||
print ("RETURNING early in fusion_run....");
|
||
return;
|
||
|
||
for (int i = 0; i < 60; i++) {
|
||
if (stopping) {
|
||
break; // not an error
|
||
}
|
||
|
||
if (Util.walletHasTransaction(txid)) {
|
||
break;
|
||
}
|
||
|
||
await Future.delayed(Duration(seconds: 1));
|
||
}
|
||
|
||
// Set status to 'complete' with 'time_wait'
|
||
status = Tuple('complete', 'txid: $txid');
|
||
|
||
// Wait for transaction to show up in wallets
|
||
// Set status to 'complete' with txid
|
||
|
||
} on FusionError catch(err) {
|
||
print('Failed: ${err}');
|
||
status.item1 = "failed";
|
||
status.item2 = err.toString(); // setting the error message
|
||
} catch(exc) {
|
||
print('Exception: ${exc}');
|
||
status.item1 = "failed";
|
||
status.item2= "Exception: ${exc.toString()}"; // setting the exception message
|
||
} finally {
|
||
clear_coins();
|
||
if (status.item1 != 'complete') {
|
||
for (var output in outputs) {
|
||
Util.unreserve_change_address(output.addr);
|
||
}
|
||
if (!server_connected_and_greeted) {
|
||
notify_server_status(false, tup: status);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
} // end fusion_run function.
|
||
|
||
|
||
|
||
|
||
|
||
|
||
void notify_server_status(bool b, {Tuple? tup}) {
|
||
// Function implementation goes here
|
||
}
|
||
|
||
|
||
void stop([String reason = 'stopped', bool notIfRunning = false]) {
|
||
if (stopping) {
|
||
return;
|
||
}
|
||
if (notIfRunning) {
|
||
if (stopping_if_not_running) {
|
||
return;
|
||
}
|
||
stopReason = reason;
|
||
stopping_if_not_running = true;
|
||
} else {
|
||
stopReason = reason;
|
||
stopping = true;
|
||
}
|
||
// note the reason is only overwritten if we were not already stopping this way.
|
||
}
|
||
|
||
void check_stop({bool running = true}) {
|
||
// Gets called occasionally from fusion thread to allow a stop point.
|
||
if (stopping || (!running && stopping_if_not_running)) {
|
||
throw FusionError(stopReason ?? 'Unknown stop reason');
|
||
}
|
||
}
|
||
|
||
void check_coins() {
|
||
// Implement by calling wallet layer to check the coins are ok.
|
||
return;
|
||
}
|
||
|
||
static void foo() {
|
||
print ("hello");
|
||
}
|
||
|
||
void clear_coins() {
|
||
coins = [];
|
||
}
|
||
|
||
void addCoins(List<Input> newCoins) {
|
||
coins.addAll(newCoins);
|
||
}
|
||
|
||
void notify_coins_UI() {
|
||
return;
|
||
}
|
||
|
||
static bool walletCanFuse() {
|
||
return true;
|
||
|
||
// Implement logic here to return false if the wallet can't fuse. (If its read only or non P2PKH)
|
||
}
|
||
|
||
static double nextDoubleNonZero(Random rng) {
|
||
double value = 0.0;
|
||
while (value == 0.0) {
|
||
value = rng.nextDouble();
|
||
}
|
||
return value;
|
||
}
|
||
|
||
static List<int>? randomOutputsForTier(Random rng, int inputAmount, int scale, int offset, int maxCount) {
|
||
|
||
if (inputAmount < offset) {
|
||
return [];
|
||
}
|
||
double lambd = 1.0 / scale;
|
||
int remaining = inputAmount;
|
||
List<double> values = []; // list of fractional random values without offset
|
||
bool didBreak = false; // Add this flag to detect when a break is encountered
|
||
|
||
for (int i = 0; i < maxCount + 1; i++) {
|
||
double val = -lambd * log(nextDoubleNonZero(rng));
|
||
remaining -= (val.ceil() + offset);
|
||
if (remaining < 0) {
|
||
didBreak = true; // If you break, set this flag to true
|
||
break;
|
||
}
|
||
values.add(val);
|
||
}
|
||
|
||
if (!didBreak && values.length > maxCount) {
|
||
values = values.sublist(0, maxCount);
|
||
}
|
||
|
||
if (values.isEmpty) {
|
||
// Our first try put us over the limit, so we have nothing to work with.
|
||
// (most likely, scale was too large)
|
||
return [];
|
||
}
|
||
|
||
int desiredRandomSum = inputAmount - values.length * offset;
|
||
assert(desiredRandomSum >= 0, 'desiredRandomSum is less than 0');
|
||
|
||
/*Now we need to rescale and round the values so they fill up the desired.
|
||
input amount exactly. We perform rounding in cumulative space so that the
|
||
sum is exact, and the rounding is distributed fairly.
|
||
*/
|
||
|
||
// Dart equivalent of itertools.accumulate
|
||
List<double> cumsum = [];
|
||
double sum = 0;
|
||
for (double value in values) {
|
||
sum += value;
|
||
cumsum.add(sum);
|
||
}
|
||
|
||
double rescale = desiredRandomSum / cumsum[cumsum.length - 1];
|
||
List<int> normedCumsum = cumsum.map((v) => (rescale * v).round()).toList();
|
||
assert(normedCumsum[normedCumsum.length - 1] == desiredRandomSum, 'Last element of normedCumsum is not equal to desiredRandomSum');
|
||
List<int> differences = [];
|
||
differences.add(normedCumsum[0]); // First element
|
||
for (int i = 1; i < normedCumsum.length; i++) {
|
||
differences.add(normedCumsum[i] - normedCumsum[i - 1]);
|
||
}
|
||
|
||
List<int> result = differences.map((d) => offset + d).toList();
|
||
assert(result.reduce((a, b) => a + b) == inputAmount, 'Sum of result is not equal to inputAmount');
|
||
return result;
|
||
|
||
}
|
||
|
||
|
||
static List<ComponentResult> genComponents(int numBlanks, List<Input> inputs, List<Output> outputs, int feerate) {
|
||
assert(numBlanks >= 0);
|
||
|
||
List<Tuple<Component, int>> components = [];
|
||
|
||
// Set up Pedersen setup instance
|
||
Uint8List HBytes = Uint8List.fromList([0x02] + 'CashFusion gives us fungibility.'.codeUnits);
|
||
ECDomainParameters params = ECDomainParameters('secp256k1');
|
||
ECPoint? HMaybe = params.curve.decodePoint(HBytes);
|
||
if (HMaybe == null) {
|
||
throw Exception('Failed to decode point');
|
||
}
|
||
ECPoint H = HMaybe;
|
||
PedersenSetup setup = PedersenSetup(H);
|
||
|
||
for (Input input in inputs) {
|
||
int fee = Util.componentFee(input.sizeOfInput(), feerate);
|
||
|
||
var comp = Component();
|
||
comp.input = InputComponent(
|
||
prevTxid: Uint8List.fromList(input.prevTxid.reversed.toList()),
|
||
prevIndex: input.prevIndex,
|
||
pubkey: input.pubKey,
|
||
amount: Int64(input.amount)
|
||
);
|
||
components.add(Tuple<Component, int>(comp, input.amount - fee));
|
||
}
|
||
|
||
for (Output output in outputs) {
|
||
var script = output.addr.toScript();
|
||
int fee = Util.componentFee(output.sizeOfOutput(), feerate);
|
||
|
||
var comp = Component();
|
||
comp.output = OutputComponent(
|
||
scriptpubkey: script,
|
||
amount: Int64(output.value)
|
||
);
|
||
components.add(Tuple<Component, int>(comp, -output.value - fee));
|
||
}
|
||
|
||
for (int i = 0; i < numBlanks; i++) {
|
||
var comp = Component();
|
||
comp.blank = BlankComponent();
|
||
components.add(Tuple<Component, int>(comp, 0));
|
||
}
|
||
|
||
List<ComponentResult> resultList = [];
|
||
|
||
components.asMap().forEach((cnum, Tuple<Component, int> componentTuple) {
|
||
Uint8List salt = Util.tokenBytes(32);
|
||
componentTuple.item1.saltCommitment = Util.sha256(salt);
|
||
var compser = componentTuple.item1.writeToBuffer();
|
||
|
||
Tuple<Uint8List, Uint8List> keyPair = Util.genKeypair();
|
||
Uint8List privateKey = keyPair.item1;
|
||
Uint8List pubKey = keyPair.item2;
|
||
|
||
Commitment commitmentInstance = setup.commit(BigInt.from(componentTuple.item2));
|
||
Uint8List amountCommitment = commitmentInstance.PUncompressed;
|
||
|
||
|
||
// Convert BigInt nonce to Uint8List
|
||
Uint8List pedersenNonce = Uint8List.fromList([int.parse(commitmentInstance.nonce.toRadixString(16), radix: 16)]);
|
||
|
||
// Generating initial commitment
|
||
InitialCommitment commitment = InitialCommitment(
|
||
saltedComponentHash: Util.sha256(Uint8List.fromList([...compser, ...salt])),
|
||
amountCommitment: amountCommitment,
|
||
communicationKey: pubKey
|
||
);
|
||
|
||
Uint8List commitser = commitment.writeToBuffer();
|
||
|
||
// Generating proof
|
||
Proof proof = Proof(
|
||
componentIdx: cnum,
|
||
salt: salt,
|
||
pedersenNonce: pedersenNonce
|
||
);
|
||
|
||
// Adding result to list
|
||
resultList.add(ComponentResult(commitser, cnum, compser, proof, privateKey));
|
||
});
|
||
|
||
return resultList;
|
||
}
|
||
|
||
|
||
Future<GeneratedMessage> recv2(SocketWrapper socketwrapper, List<String> expectedMsgNames, {Duration? timeout}) async {
|
||
if (connection == null) {
|
||
throw FusionError('Connection not initialized');
|
||
}
|
||
|
||
var result = await recvPb2(
|
||
socketwrapper,
|
||
connection!,
|
||
ServerMessage,
|
||
expectedMsgNames,
|
||
timeout: timeout
|
||
);
|
||
|
||
var submsg = result.item1;
|
||
var mtype = result.item2;
|
||
|
||
if (mtype == 'error') {
|
||
throw FusionError('server error: ${submsg.toString()}');
|
||
}
|
||
|
||
return submsg;
|
||
}
|
||
|
||
|
||
|
||
Future<GeneratedMessage> recv(List<String> expectedMsgNames, {Duration? timeout}) async {
|
||
// DEPRECATED
|
||
if (connection == null) {
|
||
throw FusionError('Connection not initialized');
|
||
}
|
||
|
||
var result = await recvPb(
|
||
connection!,
|
||
ServerMessage,
|
||
expectedMsgNames,
|
||
timeout: timeout
|
||
);
|
||
|
||
var submsg = result.item1;
|
||
var mtype = result.item2;
|
||
|
||
if (mtype == 'error') {
|
||
throw FusionError('server error: ${submsg.toString()}');
|
||
}
|
||
|
||
return submsg;
|
||
}
|
||
|
||
|
||
Future<void> send(GeneratedMessage submsg, {Duration? timeout}) async {
|
||
// DEPRECATED
|
||
if (connection != null) {
|
||
await sendPb(connection!, ClientMessage, submsg, timeout: timeout);
|
||
} else {
|
||
print('Connection is null');
|
||
}
|
||
|
||
}
|
||
|
||
|
||
Future<void> send2(SocketWrapper socketwrapper, GeneratedMessage submsg, {Duration? timeout}) async {
|
||
|
||
if (connection != null) {
|
||
await sendPb2(socketwrapper, connection!, ClientMessage, submsg, timeout: timeout);
|
||
} else {
|
||
print('Connection is null');
|
||
}
|
||
|
||
}
|
||
|
||
Future<void> greet(SocketWrapper socketwrapper) async {
|
||
ClientHello clientHello = ClientHello(
|
||
version: Uint8List.fromList(utf8.encode(Protocol.VERSION)),
|
||
genesisHash: Util.get_current_genesis_hash());
|
||
|
||
ClientMessage clientMessage = ClientMessage()
|
||
..clienthello = clientHello;
|
||
|
||
//deprecated
|
||
//Connection greet_connection_1 = Connection.withoutSocket();
|
||
|
||
/*
|
||
lets move this up a level to the fusion_run and pass it in....
|
||
SocketWrapper socketwrapper = SocketWrapper(server_host, server_port);
|
||
await socketwrapper.connect();
|
||
*/
|
||
send2(socketwrapper,clientMessage);
|
||
|
||
|
||
var replyMsg = await recv2(socketwrapper, ['serverhello']);
|
||
if (replyMsg is ServerMessage) {
|
||
ServerHello reply = replyMsg.serverhello;
|
||
|
||
numComponents = reply.numComponents;
|
||
componentFeeRate = reply.componentFeerate.toDouble();
|
||
minExcessFee = reply.minExcessFee.toDouble();
|
||
maxExcessFee = reply.maxExcessFee.toDouble();
|
||
availableTiers = reply.tiers.map((tier) => tier.toInt()).toList();
|
||
|
||
// Enforce some sensible limits, in case server is crazy
|
||
if (componentFeeRate > Protocol.MAX_COMPONENT_FEERATE) {
|
||
throw FusionError('excessive component feerate from server');
|
||
}
|
||
if (minExcessFee > 400) { // note this threshold should be far below MAX_EXCESS_FEE
|
||
throw FusionError('excessive min excess fee from server');
|
||
}
|
||
if (minExcessFee > maxExcessFee) {
|
||
throw FusionError('bad config on server: fees');
|
||
}
|
||
if (numComponents < Protocol.MIN_TX_COMPONENTS * 1.5) {
|
||
throw FusionError('bad config on server: num_components');
|
||
}
|
||
} else {
|
||
throw Exception('Received unexpected message type: ${replyMsg.runtimeType}');
|
||
}
|
||
}
|
||
|
||
|
||
Future<void> allocateOutputs(socketwrapper) async {
|
||
print ("DBUG allocateoutputs 746");
|
||
|
||
print ("CHECK socketwrapper 746");
|
||
socketwrapper.status();
|
||
assert(['setup', 'connecting'].contains(status.item1));
|
||
|
||
List<Input> inputs = coins;
|
||
int numInputs = inputs.length;
|
||
|
||
int maxComponents = min(numComponents, Protocol.MAX_COMPONENTS);
|
||
int maxOutputs = maxComponents - numInputs;
|
||
if (maxOutputs < 1) {
|
||
throw FusionError('Too many inputs ($numInputs >= $maxComponents)');
|
||
}
|
||
|
||
if (maxOutputs != null) {
|
||
assert(maxOutputs >= 1);
|
||
maxOutputs = min(maxOutputs, maxOutputs);
|
||
}
|
||
|
||
int numDistinct = inputs.map((e) => e.value).toSet().length;
|
||
int minOutputs = max(Protocol.MIN_TX_COMPONENTS - numDistinct, 1);
|
||
if (maxOutputs < minOutputs) {
|
||
throw FusionError('Too few distinct inputs selected ($numDistinct); cannot satisfy output count constraint (>= $minOutputs, <= $maxOutputs)');
|
||
}
|
||
|
||
|
||
int sumInputsValue = inputs.map((e) => e.value).reduce((a, b) => a + b);
|
||
int inputFees = inputs.map((e) => Util.componentFee(e.sizeOfInput(), componentFeeRate.toInt())).reduce((a, b) => a + b);
|
||
int availForOutputs = sumInputsValue - inputFees - minExcessFee.toInt();
|
||
|
||
int feePerOutput = Util.componentFee(34, componentFeeRate.toInt());
|
||
|
||
int offsetPerOutput = Protocol.MIN_OUTPUT + feePerOutput;
|
||
|
||
if (availForOutputs < offsetPerOutput) {
|
||
throw FusionError('Selected inputs had too little value');
|
||
}
|
||
|
||
var rng = Random();
|
||
var seed = List<int>.generate(32, (_) => rng.nextInt(256));
|
||
|
||
print ("DBUG allocateoutputs 785");
|
||
tierOutputs = {};
|
||
var excessFees = <int, int>{};
|
||
for (var scale in availableTiers) {
|
||
int fuzzFeeMax = scale ~/ 1000000;
|
||
int fuzzFeeMaxReduced = min(fuzzFeeMax, min(Protocol.MAX_EXCESS_FEE - minExcessFee.toInt(), maxExcessFee.toInt()));
|
||
|
||
assert(fuzzFeeMaxReduced >= 0);
|
||
int fuzzFee = rng.nextInt(fuzzFeeMaxReduced + 1);
|
||
|
||
int reducedAvailForOutputs = availForOutputs - fuzzFee;
|
||
if (reducedAvailForOutputs < offsetPerOutput) {
|
||
continue;
|
||
}
|
||
|
||
var outputs = randomOutputsForTier(rng, reducedAvailForOutputs, scale, offsetPerOutput, maxOutputs);
|
||
if (outputs != null) {
|
||
print (outputs);
|
||
}
|
||
if (outputs == null || outputs.length < minOutputs) {
|
||
|
||
|
||
continue;
|
||
}
|
||
outputs = outputs.map((o) => o - feePerOutput).toList();
|
||
|
||
assert(inputs.length + (outputs?.length ?? 0) <= Protocol.MAX_COMPONENTS);
|
||
|
||
excessFees[scale] = sumInputsValue - inputFees - reducedAvailForOutputs;
|
||
tierOutputs[scale] = outputs!;
|
||
}
|
||
|
||
|
||
print('Possible tiers: $tierOutputs');
|
||
|
||
safety_sum_in = sumInputsValue;
|
||
safety_exess_fees = excessFees;
|
||
return;
|
||
}
|
||
|
||
|
||
|
||
Future<void> registerAndWait(SocketWrapper socketwrapper) async {
|
||
|
||
var stopwatch = Stopwatch()..start();
|
||
// msg can be different classes depending on which protobuf msg is sent.
|
||
dynamic? msg;
|
||
|
||
var tierOutputs = this.tierOutputs;
|
||
var tiersSorted = tierOutputs.keys.toList()..sort();
|
||
|
||
if (tierOutputs.isEmpty) {
|
||
throw FusionError('No outputs available at any tier (selected inputs were too small / too large).');
|
||
}
|
||
|
||
print('registering for tiers: $tiersSorted');
|
||
|
||
int self_fuse = 1; // Temporary value for now
|
||
var cashfusionTag = [1];// temp value for now
|
||
|
||
|
||
|
||
check_stop(running: false);
|
||
check_coins();
|
||
|
||
|
||
var tags = [JoinPools_PoolTag(id: cashfusionTag, limit: self_fuse)];
|
||
|
||
// Create JoinPools message
|
||
JoinPools joinPools = JoinPools(
|
||
tiers: tiersSorted.map((i) => Int64(i)).toList(),
|
||
tags: tags
|
||
);
|
||
|
||
// Wrap it in a ClientMessage
|
||
ClientMessage clientMessage = ClientMessage()
|
||
..joinpools = joinPools;
|
||
|
||
send2(socketwrapper, clientMessage);
|
||
|
||
|
||
status = Tuple<String, String>('waiting', 'Registered for tiers');
|
||
|
||
var tiersStrings = {for (var entry in tierOutputs.entries) entry.key: (entry.key * 1e-8).toStringAsFixed(8).replaceAll(RegExp(r'0+$'), '')};
|
||
|
||
while (true) {
|
||
msg = await recv2(socketwrapper,['tierstatusupdate', 'fusionbegin'], timeout: Duration(seconds: 10));
|
||
var fieldInfoFusionBegin = msg.info_.byName["fusionbegin"];
|
||
if (fieldInfoFusionBegin != null && msg.hasField(fieldInfoFusionBegin.tagNumber)) {
|
||
print ("DEBUG 867 Fusion Begin message...");
|
||
break;
|
||
}
|
||
|
||
check_stop(running: false);
|
||
check_coins();
|
||
|
||
// Define the bool variable
|
||
|
||
|
||
|
||
var fieldInfo = msg.info_.byName["tierstatusupdate"];
|
||
if (fieldInfo == null) {
|
||
throw FusionError('Expected field not found in message: tierstatusupdate');
|
||
}
|
||
|
||
bool messageIsTierStatusUpdate = msg.hasField(fieldInfo.tagNumber);
|
||
print ("DEBUG 889 getting tier update.");
|
||
|
||
if (!messageIsTierStatusUpdate) {
|
||
throw FusionError('Expected a TierStatusUpdate message');
|
||
}
|
||
|
||
late var statuses;
|
||
if (messageIsTierStatusUpdate) {
|
||
//TierStatusUpdate tierStatusUpdate = msg.tierstatusupdate;
|
||
var tierStatusUpdate = msg.getField(fieldInfo.tagNumber) as TierStatusUpdate;
|
||
statuses = tierStatusUpdate.statuses;
|
||
}
|
||
|
||
|
||
double maxfraction = 0.0;
|
||
var maxtiers = <int>[];
|
||
int? besttime;
|
||
int? besttimetier;
|
||
for (var entry in statuses.entries) {
|
||
double frac = entry.value.players.toInt() / entry.value.minPlayers.toInt();
|
||
if (frac >= maxfraction) {
|
||
if (frac > maxfraction) {
|
||
maxfraction = frac;
|
||
maxtiers.clear();
|
||
}
|
||
maxtiers.add(entry.key.toInt());
|
||
}
|
||
|
||
var fieldInfoTimeRemaining = entry.value.info_.byName["timeRemaining"];
|
||
|
||
if (entry.value.hasField(fieldInfoTimeRemaining.tagNumber)) {
|
||
int tr = entry.value.timeRemaining.toInt();
|
||
if (besttime == null || tr < besttime) {
|
||
besttime = tr;
|
||
besttimetier = entry.key.toInt();
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
var displayBest = <String>[];
|
||
var displayMid = <String>[];
|
||
var displayQueued = <String>[];
|
||
|
||
for (var tier in tiersSorted) {
|
||
|
||
if (statuses.containsKey(tier)) {
|
||
var tierStr = tiersStrings[tier];
|
||
if (tierStr == null) {
|
||
throw FusionError('server reported status on tier we are not registered for');
|
||
}
|
||
if (tier == besttimetier) {
|
||
displayBest.insert(0, '**$tierStr**');
|
||
} else if (maxtiers.contains(tier)) {
|
||
displayBest.add('[$tierStr]');
|
||
} else {
|
||
displayMid.add(tierStr);
|
||
}
|
||
} else {
|
||
displayQueued.add(tiersStrings[tier]!);
|
||
}
|
||
}
|
||
|
||
|
||
var parts = <String>[];
|
||
if (displayBest.isNotEmpty || displayMid.isNotEmpty) {
|
||
parts.add("Tiers: ${displayBest.join(', ')} ${displayMid.join(', ')}");
|
||
}
|
||
if (displayQueued.isNotEmpty) {
|
||
parts.add("Queued: ${displayQueued.join(', ')}");
|
||
}
|
||
var tiersString = parts.join(' ');
|
||
|
||
if (besttime == null && inactiveTimeLimit != null) {
|
||
if (stopwatch.elapsedMilliseconds > inactiveTimeLimit) {
|
||
throw FusionError('stopping due to inactivity');
|
||
} else {
|
||
;
|
||
}
|
||
} else {
|
||
;
|
||
}
|
||
|
||
|
||
if (besttime != null) {
|
||
status = Tuple<String, String>('waiting', 'Starting in ${besttime}s. $tiersString');
|
||
} else if (maxfraction >= 1) {
|
||
status = Tuple<String, String>('waiting', 'Starting soon. $tiersString');
|
||
} else if (displayBest.isNotEmpty || displayMid.isNotEmpty) {
|
||
status = Tuple<String, String>('waiting', '${(maxfraction * 100).round()}% full. $tiersString');
|
||
} else {
|
||
status = Tuple<String, String>('waiting', tiersString);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
var fieldInfoFusionBegin = msg.info_.byName["fusionbegin"];
|
||
|
||
// Check if fieldInfoFusionBegin is null
|
||
if (fieldInfoFusionBegin == null) {
|
||
throw FusionError('Expected field not found in message: fusionbegin');
|
||
}
|
||
|
||
bool messageIsFusionBegin = msg.hasField(fieldInfoFusionBegin.tagNumber);
|
||
|
||
// Print whether the message is FusionBegin or not
|
||
if (!messageIsFusionBegin) {
|
||
throw FusionError('Expected a FusionBegin message');
|
||
}
|
||
|
||
|
||
|
||
t_fusionBegin = DateTime.now();
|
||
|
||
FusionBegin fusionBeginMsg = msg.fusionbegin;
|
||
|
||
var elapsedSeconds = (stopwatch.elapsedMilliseconds / 1000).toInt();
|
||
var clockMismatch = fusionBeginMsg.serverTime.toInt() - DateTime.now().millisecondsSinceEpoch / 1000;
|
||
|
||
|
||
|
||
if (clockMismatch.abs().toDouble() > Protocol.MAX_CLOCK_DISCREPANCY) {
|
||
throw FusionError("Clock mismatch too large: ${(clockMismatch.toDouble()).toStringAsFixed(3)}.");
|
||
}
|
||
|
||
|
||
tier = fusionBeginMsg.tier.toInt();
|
||
|
||
if (msg is FusionBegin) {
|
||
covertDomainB = Uint8List.fromList(msg.covertDomain);
|
||
}
|
||
|
||
covertPort = fusionBeginMsg.covertPort;
|
||
covertSSL = fusionBeginMsg.covertSsl;
|
||
beginTime = fusionBeginMsg.serverTime.toDouble();
|
||
|
||
lastHash = Util.calcInitialHash(tier, covertDomainB, covertPort, covertSSL, beginTime);
|
||
|
||
var outAmounts = tierOutputs[tier];
|
||
var outAddrs = Util.reserve_change_addresses(outAmounts?.length ?? 0);
|
||
|
||
reservedAddresses = outAddrs;
|
||
outputs = Util.zip(outAmounts ?? [], outAddrs).map((pair) => Output(value: pair[0], addr: pair[1])).toList();
|
||
|
||
safetyExcessFee = safety_exess_fees[tier] ?? 0;
|
||
|
||
print("starting fusion rounds at tier $tier: ${coins.length} inputs and ${outputs.length} outputs");
|
||
}
|
||
|
||
Future<CovertSubmitter> startCovert() async {
|
||
|
||
print ("DEBUG START COVERT!");
|
||
status = Tuple('running', 'Setting up Tor connections');
|
||
|
||
|
||
String covertDomain;
|
||
try {
|
||
covertDomain = utf8.decode(covertDomainB);
|
||
} catch (e) {
|
||
throw FusionError('badly encoded covert domain');
|
||
}
|
||
CovertSubmitter covert = CovertSubmitter(
|
||
covertDomain,
|
||
covertPort,
|
||
covertSSL,
|
||
tor_host,
|
||
tor_port,
|
||
numComponents,
|
||
Protocol.COVERT_SUBMIT_WINDOW,
|
||
Protocol.COVERT_SUBMIT_TIMEOUT
|
||
);
|
||
try {
|
||
covert.scheduleConnections(
|
||
t_fusionBegin,
|
||
Duration(seconds: Protocol.COVERT_CONNECT_WINDOW.toInt()),
|
||
numSpares: Protocol.COVERT_CONNECT_SPARES.toInt(),
|
||
connectTimeout: Protocol.COVERT_CONNECT_TIMEOUT.toInt()
|
||
);
|
||
|
||
print ("DEBUG return early from covert");
|
||
return covert;
|
||
|
||
// loop until a just a bit before we're expecting startRound, watching for status updates
|
||
final tend = t_fusionBegin.add(Duration(seconds: (Protocol.WARMUP_TIME - Protocol.WARMUP_SLOP - 1).round()));
|
||
|
||
while (DateTime.now().millisecondsSinceEpoch / 1000 < tend.millisecondsSinceEpoch / 1000) {
|
||
|
||
int numConnected = covert.slots.where((s) => s.covConn?.connection != null).length;
|
||
|
||
int numSpareConnected = covert.spareConnections.where((c) => c.connection != null).length;
|
||
|
||
status = Tuple('running', 'Setting up Tor connections ($numConnected+$numSpareConnected out of $numComponents)');
|
||
|
||
await Future.delayed(Duration(seconds: 1));
|
||
|
||
covert.checkOk();
|
||
this.check_stop();
|
||
this.check_coins();
|
||
}
|
||
} catch (e) {
|
||
covert.stop();
|
||
rethrow;
|
||
}
|
||
|
||
return covert;
|
||
}
|
||
|
||
|
||
Future<bool> runRound(CovertSubmitter covert) async {
|
||
print ("START OF RUN ROUND");
|
||
|
||
status = Tuple('running', 'Starting round ${roundcount.toString()}');
|
||
int timeoutInSeconds = (2 * Protocol.WARMUP_SLOP + Protocol.STANDARD_TIMEOUT).toInt();
|
||
var msg = await recv(['startround'], timeout: Duration(seconds: timeoutInSeconds));
|
||
|
||
// Record the time we got this message; it forms the basis time for all covert activities.
|
||
final covertT0 = DateTime.now().millisecondsSinceEpoch / 1000;
|
||
double covertClock() => (DateTime.now().millisecondsSinceEpoch / 1000) - covertT0;
|
||
|
||
final roundTime = (msg as StartRound).serverTime;
|
||
|
||
// Check the server's declared unix time, which will be committed.
|
||
final clockMismatch = (msg as StartRound).serverTime - DateTime.now().millisecondsSinceEpoch / 1000;
|
||
if (clockMismatch.abs() > Protocol.MAX_CLOCK_DISCREPANCY) {
|
||
throw FusionError("Clock mismatch too large: ${clockMismatch.toInt().toStringAsPrecision(3)}.");
|
||
}
|
||
|
||
if (t_fusionBegin != null) {
|
||
// On the first startround message, check that the warmup time was within acceptable bounds.
|
||
final lag = covertT0 - (t_fusionBegin.millisecondsSinceEpoch / 1000) - Protocol.WARMUP_TIME;
|
||
if (lag.abs() > Protocol.WARMUP_SLOP) {
|
||
throw FusionError("Warmup period too different from expectation (|${lag.toStringAsFixed(3)}s| > ${Protocol.WARMUP_SLOP.toStringAsFixed(3)}s).");
|
||
}
|
||
t_fusionBegin = DateTime.now();
|
||
}
|
||
|
||
print("round starting at ${DateTime.now().millisecondsSinceEpoch / 1000}");
|
||
|
||
final inputFees = coins.map((e) => Util.componentFee(e.sizeOfInput(), componentFeeRate.toInt())).reduce((a, b) => a + b);
|
||
final outputFees = outputs.length * Util.componentFee(34, componentFeeRate.toInt());
|
||
|
||
final sumIn = coins.map((e) => e.amount).reduce((a, b) => a + b);
|
||
final sumOut = outputs.map((e) => e.value).reduce((a, b) => a + b);
|
||
|
||
final totalFee = sumIn - sumOut;
|
||
final excessFee = totalFee - inputFees - outputFees;
|
||
final safeties = [
|
||
sumIn == safety_sum_in,
|
||
excessFee == safetyExcessFee,
|
||
excessFee <= Protocol.MAX_EXCESS_FEE,
|
||
totalFee <= Protocol.MAX_FEE,
|
||
];
|
||
|
||
if (!safeties.every((element) => element)) {
|
||
throw Exception("(BUG!) Funds re-check failed -- aborting for safety. ${safeties.toString()}");
|
||
}
|
||
|
||
final roundPubKey = (msg as StartRound).roundPubkey;
|
||
|
||
final blindNoncePoints = (msg as StartRound).blindNoncePoints;
|
||
if (blindNoncePoints.length != numComponents) {
|
||
throw FusionError('blind nonce miscount');
|
||
}
|
||
|
||
final numBlanks = numComponents - coins.length - outputs.length;
|
||
final List<ComponentResult> genComponentsResults = genComponents(numBlanks, coins, outputs, componentFeeRate.toInt());
|
||
|
||
final List<Uint8List> myCommitments = [];
|
||
final List<int> myComponentSlots = [];
|
||
final List<Uint8List> myComponents = [];
|
||
final List<Proof> myProofs = [];
|
||
final List<Uint8List> privKeys = [];
|
||
final List<dynamic> pedersenAmount = []; // replace dynamic with the actual type
|
||
final List<dynamic> pedersenNonce = []; // replace dynamic with the actual type
|
||
|
||
for (var genComponentResult in genComponentsResults) {
|
||
myCommitments.add(genComponentResult.commitment);
|
||
myComponentSlots.add(genComponentResult.counter);
|
||
myComponents.add(genComponentResult.component);
|
||
myProofs.add(genComponentResult.proof);
|
||
privKeys.add(genComponentResult.privateKey);
|
||
pedersenAmount.add(genComponentResult.pedersenAmount);
|
||
pedersenNonce.add(genComponentResult.pedersenNonce);
|
||
}
|
||
assert(excessFee == pedersenAmount.reduce((a, b) => a + b)); // sanity check that we didn't mess up the above
|
||
assert(myComponents.toSet().length == myComponents.length); // no duplicates
|
||
|
||
// Need to implement this! schnorr is from EC schnorr.py
|
||
var blindSigRequests = <dynamic>[];
|
||
|
||
/*
|
||
final blindSigRequests = blindNoncePoints.map((e) => Schnorr.BlindSignatureRequest(roundPubKey, e, sha256(myComponents.elementAt(e)))).toList();
|
||
|
||
*/
|
||
print ("RETURNING EARLY FROM run round .....");
|
||
return true;
|
||
|
||
final randomNumber = Util.getRandomBytes(32);
|
||
covert.checkOk();
|
||
check_stop();
|
||
check_coins();
|
||
|
||
|
||
await send(PlayerCommit(
|
||
initialCommitments: myCommitments,
|
||
excessFee: Int64(excessFee),
|
||
pedersenTotalNonce: pedersenNonce.cast<int>(),
|
||
randomNumberCommitment: sha256.convert(randomNumber).bytes,
|
||
blindSigRequests: blindSigRequests.map((r) => r.getRequest() as List<int>).toList(),
|
||
));
|
||
|
||
|
||
msg = await recv(['blindsigresponses'], timeout: Duration(seconds: Protocol.T_START_COMPS.toInt()));
|
||
|
||
|
||
if (msg is BlindSigResponses) {
|
||
var typedMsg = msg as BlindSigResponses;
|
||
assert(typedMsg.scalars.length == blindSigRequests.length);
|
||
} else {
|
||
// Handle the case where msg is not of type BlindSigResponses
|
||
throw Exception('Unexpected message type: ${msg.runtimeType}');
|
||
}
|
||
|
||
final blindSigs = List.generate(
|
||
blindSigRequests.length,
|
||
(index) {
|
||
if (msg is BlindSigResponses) {
|
||
var typedMsg = msg as BlindSigResponses;
|
||
return blindSigRequests[index].finalize(typedMsg.scalars[index], check: true);
|
||
} else {
|
||
// Handle the case where msg is not of type BlindSigResponses
|
||
throw Exception('Unexpected message type: ${msg.runtimeType}');
|
||
}
|
||
},
|
||
);
|
||
|
||
|
||
// Sleep until the covert component phase really starts, to catch covert connection failures.
|
||
var remainingTime = Protocol.T_START_COMPS - covertClock();
|
||
if (remainingTime < 0) {
|
||
throw FusionError('Arrived at covert-component phase too slowly.');
|
||
}
|
||
await Future.delayed(Duration(seconds: remainingTime.floor()));
|
||
|
||
// Our final check to leave the fusion pool, before we start telling our
|
||
// components. This is much more annoying since it will cause the round
|
||
// to fail, but since we would end up killing the round anyway then it's
|
||
// best for our privacy if we just leave now.
|
||
// (This also is our first call to check_connected.)
|
||
covert.checkConnected();
|
||
check_coins();
|
||
|
||
// Start covert component submissions
|
||
print("starting covert component submission");
|
||
status = Tuple('running', 'covert submission: components');
|
||
|
||
// If we fail after this point, we want to stop connections gradually and
|
||
// randomly. We don't want to stop them all at once, since if we had already
|
||
// provided our input components then it would be a leak to have them all drop at once.
|
||
covert.setStopTime((covertT0 + Protocol.T_START_CLOSE).toInt());
|
||
|
||
|
||
// Schedule covert submissions.
|
||
List<CovertComponent?> messages = List.filled(myComponents.length, null);
|
||
|
||
for (var i = 0; i < myComponents.length; i++) {
|
||
messages[myComponentSlots[i]] = CovertComponent(
|
||
roundPubkey: roundPubKey,
|
||
signature: blindSigs[i],
|
||
component: myComponents[i]
|
||
);
|
||
}
|
||
if (messages.any((element) => element == null)) {
|
||
throw FusionError('Messages list includes null values.');
|
||
}
|
||
|
||
final targetDateTime = DateTime.fromMillisecondsSinceEpoch(((covertT0 + Protocol.T_START_COMPS) * 1000).toInt());
|
||
covert.scheduleSubmissions(targetDateTime, messages);
|
||
|
||
|
||
// While submitting, we download the (large) full commitment list.
|
||
msg = await recv(['allcommitments'], timeout: Duration(seconds: Protocol.T_START_SIGS.toInt()));
|
||
AllCommitments allCommitmentsMsg = msg as AllCommitments;
|
||
List<InitialCommitment> allCommitments = allCommitmentsMsg.initialCommitments.map((commitmentBytes) {
|
||
return InitialCommitment.fromBuffer(commitmentBytes);
|
||
}).toList();
|
||
|
||
|
||
// Quick check on the commitment list.
|
||
if (allCommitments.toSet().length != allCommitments.length) {
|
||
throw FusionError('Commitments list includes duplicates.');
|
||
}
|
||
try {
|
||
List<Uint8List> allCommitmentsBytes = allCommitments.map((commitment) => commitment.writeToBuffer()).toList();
|
||
myCommitmentIndexes = myCommitments.map((c) => allCommitmentsBytes.indexOf(c)).toList();
|
||
|
||
|
||
} on Exception {
|
||
throw FusionError('One or more of my commitments missing.');
|
||
}
|
||
|
||
remainingTime = Protocol.T_START_SIGS - covertClock();
|
||
if (remainingTime < 0) {
|
||
throw FusionError('took too long to download commitments list');
|
||
}
|
||
|
||
// Once all components are received, the server shares them with us:
|
||
msg = await recv(['sharecovertcomponents'], timeout: Duration(seconds: Protocol.T_START_SIGS.toInt()));
|
||
|
||
ShareCovertComponents shareCovertComponentsMsg = msg as ShareCovertComponents;
|
||
List<List<int>> allComponents = shareCovertComponentsMsg.components;
|
||
bool skipSignatures = msg.getField(2);
|
||
|
||
// Critical check on server's response timing.
|
||
if (covertClock() > Protocol.T_START_SIGS) {
|
||
throw FusionError('Shared components message arrived too slowly.');
|
||
}
|
||
|
||
covert.checkDone();
|
||
|
||
try {
|
||
myComponentIndexes = myComponents.map((c) => allComponents.indexWhere((element) => ListEquality().equals(element, c))).toList();
|
||
if (myComponentIndexes.contains(-1)) {
|
||
throw FusionError('One or more of my components missing.');
|
||
}
|
||
} on StateError {
|
||
throw FusionError('One or more of my components missing.');
|
||
}
|
||
|
||
// Need to implement: check the components list and see if there are enough inputs/outputs
|
||
// for there to be significant privacy.
|
||
|
||
List<List<int>> allCommitmentsBytes = allCommitments.map((commitment) => commitment.writeToBuffer().toList()).toList();
|
||
List<int> sessionHash = Util.calcRoundHash(lastHash, roundPubKey, roundTime.toInt(), allCommitmentsBytes, allComponents);
|
||
|
||
|
||
if (shareCovertComponentsMsg.sessionHash != null && !ListEquality().equals(shareCovertComponentsMsg.sessionHash, sessionHash)) {
|
||
|
||
throw FusionError('Session hash mismatch (bug!)');
|
||
}
|
||
|
||
if (!shareCovertComponentsMsg.skipSignatures) {
|
||
print("starting covert signature submission");
|
||
status = Tuple('running', 'covert submission: signatures');
|
||
|
||
if (allComponents
|
||
.toSet()
|
||
.length != allComponents.length) {
|
||
throw FusionError('Server component list includes duplicates.');
|
||
}
|
||
|
||
|
||
var txInputIndices = Transaction.txFromComponents(
|
||
allComponents, sessionHash);
|
||
|
||
|
||
Tuple txData = Transaction.txFromComponents(allComponents, sessionHash);
|
||
tx = txData.item1;
|
||
List<int> inputIndices = txData.item2;
|
||
|
||
|
||
List<
|
||
CovertTransactionSignature?> covertTransactionSignatureMessages = List<
|
||
CovertTransactionSignature?>.filled(myComponents.length, null);
|
||
|
||
var my_combined = List<Tuple<int, Input>>.generate(
|
||
inputIndices.length,
|
||
(index) => Tuple(inputIndices[index], tx.Inputs[index]),
|
||
);
|
||
|
||
|
||
|
||
for (var i = 0; i < my_combined.length; i++) {
|
||
int cIdx = my_combined[i].item1;
|
||
Input inp = my_combined[i].item2;
|
||
|
||
int myCompIdx = myComponentIndexes.indexOf(cIdx);
|
||
if (myCompIdx == -1) continue; // not my input
|
||
|
||
var pubKey = inp.getPubKey(0);
|
||
var sec = inp.getPrivKey(0);
|
||
|
||
var preimageBytes = tx.serializePreimage(i, 0x41, useCache: true);
|
||
var sighash = sha256.convert(sha256.convert(preimageBytes).bytes);
|
||
|
||
//var sig = schnorr.sign(sec, sighash); // Needs implementation
|
||
var sig = <int>[0, 1, 2, 3, 4]; // dummy placeholder
|
||
|
||
covertTransactionSignatureMessages[myComponentSlots[myCompIdx]] =
|
||
CovertTransactionSignature(txsignature: sig, whichInput: i);
|
||
}
|
||
|
||
DateTime covertT0DateTime = DateTime.fromMillisecondsSinceEpoch(
|
||
covertT0.toInt() * 1000); // covertT0 is in seconds
|
||
covert.scheduleSubmissions(
|
||
covertT0DateTime.add(
|
||
Duration(milliseconds: Protocol.T_START_SIGS.toInt())),
|
||
covertTransactionSignatureMessages
|
||
);
|
||
|
||
|
||
// wait for result
|
||
int timeoutMillis = (Protocol.T_EXPECTING_CONCLUSION -
|
||
Protocol.TS_EXPECTING_COVERT_COMPONENTS).toInt();
|
||
Duration timeout = Duration(milliseconds: timeoutMillis);
|
||
msg = await recv(['fusionresult'], timeout: timeout);
|
||
|
||
|
||
// Critical check on server's response timing.
|
||
if (covertClock() > Protocol.T_EXPECTING_CONCLUSION) {
|
||
throw FusionError('Fusion result message arrived too slowly.');
|
||
}
|
||
|
||
covert.checkDone();
|
||
FusionResult fusionResultMsg = msg as FusionResult;
|
||
if (fusionResultMsg.ok) {
|
||
List<List<int>> allSigs = msg.txsignatures;
|
||
|
||
// assemble the transaction.
|
||
if (allSigs.length != tx.Inputs.length) {
|
||
throw FusionError('Server gave wrong number of signatures.');
|
||
}
|
||
for (var i = 0; i < allSigs.length; i++) {
|
||
List<int> sigBytes = allSigs[i];
|
||
String sig = base64.encode(sigBytes);
|
||
Input inp = tx.Inputs[i];
|
||
if (sig.length != 64) {
|
||
throw FusionError('server relayed bad signature');
|
||
}
|
||
inp.signatures = [sig + '41'];
|
||
}
|
||
|
||
assert(tx.isComplete());
|
||
String txHex = tx.serialize();
|
||
|
||
txid = tx.txid();
|
||
String sumInStr = Util.formatSatoshis(sumIn,
|
||
numZeros: 8);
|
||
String feeStr = totalFee
|
||
.toString();
|
||
String feeLoc = 'fee';
|
||
|
||
String label = "CashFusion ${coins.length}⇢${outputs
|
||
.length}, ${sumInStr} BCH (−${feeStr} sats ${feeLoc})";
|
||
|
||
Util.updateWalletLabel(txid, label);
|
||
}
|
||
|
||
else {
|
||
badComponents = msg.badComponents.toSet();
|
||
if (badComponents.intersection(myComponentIndexes.toSet()).isNotEmpty) {
|
||
print("bad components: ${badComponents.toList()} mine: ${myComponentIndexes.toList()}");
|
||
throw FusionError("server thinks one of my components is bad!");
|
||
}
|
||
}
|
||
|
||
|
||
}
|
||
else { // skip_signatures True
|
||
Set<int> badComponents = Set<int>();
|
||
}
|
||
|
||
// ### Blame phase ###
|
||
|
||
|
||
covert.setStopTime((covertT0 + Protocol.T_START_CLOSE_BLAME).floor());
|
||
|
||
print("sending proofs");
|
||
status = Tuple('running', 'round failed - sending proofs');
|
||
|
||
|
||
// create a list of commitment indexes, but leaving out mine.
|
||
List<int> othersCommitmentIdxes = [];
|
||
for(int i=0; i<allCommitments.length; i++) {
|
||
if (!myCommitmentIndexes.contains(i)) {
|
||
othersCommitmentIdxes.add(i);
|
||
}
|
||
}
|
||
int N = othersCommitmentIdxes.length;
|
||
assert(N == allCommitments.length - myCommitments.length);
|
||
if (N == 0) {
|
||
throw FusionError("Fusion failed with only me as player -- I can only blame myself.");
|
||
}
|
||
|
||
// where should I send my proofs?
|
||
List<InitialCommitment> dstCommits = [];
|
||
for(int i=0; i<myCommitments.length; i++) {
|
||
dstCommits.add(allCommitments[othersCommitmentIdxes[Util.randPosition(randomNumber, N, i)]]);
|
||
}
|
||
|
||
// generate the encrypted proofs
|
||
List<String> encproofs = List<String>.filled(myCommitments.length, '');
|
||
|
||
|
||
ECDomainParameters params = ECDomainParameters('secp256k1');
|
||
for (int i=0; i<dstCommits.length; i++) {
|
||
InitialCommitment msg = dstCommits[i];
|
||
Proof proof = myProofs[i];
|
||
proof.componentIdx = myComponentIndexes[i];
|
||
|
||
ECPoint? communicationKeyPointMaybe = params.curve.decodePoint(Uint8List.fromList(msg.communicationKey));
|
||
if (communicationKeyPointMaybe == null) {
|
||
// handle the error case here, e.g., throw an exception or skip this iteration.
|
||
continue;
|
||
}
|
||
ECPoint communicationKeyPoint = communicationKeyPointMaybe;
|
||
|
||
try {
|
||
Uint8List encryptedData = await encrypt(proof.writeToBuffer(), communicationKeyPoint, padToLength: 80);
|
||
encproofs[i] = String.fromCharCodes(encryptedData);
|
||
} catch (EncryptionFailed) {
|
||
// The communication key was bad (probably invalid x coordinate).
|
||
// We will just send a blank. They can't even blame us since there is no private key! :)
|
||
continue;
|
||
}
|
||
}
|
||
|
||
List<Uint8List> encodedEncproofs = encproofs.map((e) => Uint8List.fromList(e.codeUnits)).toList();
|
||
this.send(MyProofsList(encryptedProofs: encodedEncproofs, randomNumber: randomNumber));
|
||
|
||
status = Tuple('running', 'round failed - checking proofs');
|
||
|
||
print("receiving proofs");
|
||
msg = await this.recv(['theirproofslist'], timeout: Duration(seconds: (2 * Protocol.STANDARD_TIMEOUT).round()));
|
||
|
||
List<Blames_BlameProof> blames = [];
|
||
|
||
int countInputs = 0;
|
||
|
||
TheirProofsList proofsList = msg as TheirProofsList;
|
||
|
||
var privKey;
|
||
var commitmentBlob;
|
||
for (var i = 0; i < proofsList.proofs.length; i++) {
|
||
var rp = msg.proofs[i];
|
||
try {
|
||
privKey = privKeys[rp.dstKeyIdx];
|
||
commitmentBlob = allCommitments[rp.srcCommitmentIdx];
|
||
} on RangeError catch (e) {
|
||
throw FusionError("Server relayed bad proof indices");
|
||
}
|
||
|
||
var sKey;
|
||
var proofBlob;
|
||
|
||
|
||
|
||
try {
|
||
var result = await decrypt(Uint8List.fromList(rp.encryptedProof), privKey);
|
||
proofBlob = result.item1; // First item is the decrypted data
|
||
sKey = result.item2; // Second item is the symmetric key
|
||
} on Exception catch (e) {
|
||
print("found an undecryptable proof");
|
||
blames.add(Blames_BlameProof(whichProof: i, privkey: privKey, blameReason: 'undecryptable'));
|
||
continue;
|
||
}
|
||
|
||
|
||
var commitment = InitialCommitment();
|
||
try {
|
||
commitment.mergeFromBuffer(commitmentBlob); // Method to parse protobuf data
|
||
} on FormatException catch (e) {
|
||
throw FusionError("Server relayed bad commitment");
|
||
}
|
||
|
||
var inpComp;
|
||
|
||
try {
|
||
// Convert allComponents to List<Uint8List>
|
||
List<Uint8List> allComponentsUint8 = allComponents.map((component) => Uint8List.fromList(component)).toList();
|
||
// Convert badComponents to List<int>
|
||
List<int> badComponentsList = badComponents.toList();
|
||
// Convert componentFeeRate to int if it's double
|
||
int componentFeerateInt = componentFeeRate.round(); // or use .toInt() if you want to truncate instead of rounding
|
||
|
||
var inpComp = validateProofInternal(proofBlob, commitment, allComponentsUint8, badComponentsList, componentFeerateInt);
|
||
} on Exception catch (e) {
|
||
print("found an erroneous proof: ${e.toString()}");
|
||
var blameProof = Blames_BlameProof();
|
||
blameProof.whichProof = i;
|
||
blameProof.sessionKey = sKey;
|
||
blameProof.blameReason = e.toString();
|
||
blames.add(blameProof);
|
||
continue;
|
||
}
|
||
|
||
|
||
if (inpComp != null) {
|
||
countInputs++;
|
||
try {
|
||
Util.checkInputElectrumX(inpComp);
|
||
} on Exception catch (e) {
|
||
print("found a bad input [${rp.srcCommitmentIdx}]: $e (${inpComp.prevTxid.reversed.toList().toHex()}:${inpComp.prevIndex})");
|
||
|
||
var blameProof = Blames_BlameProof();
|
||
blameProof.whichProof = i;
|
||
blameProof.sessionKey = sKey;
|
||
blameProof.blameReason = 'input does not match blockchain: ' + e.toString();
|
||
blameProof.needLookupBlockchain = true;
|
||
blames.add(blameProof);
|
||
|
||
} catch (e) {
|
||
print("verified an input internally, but was unable to check it against blockchain: ${e}");
|
||
}
|
||
}
|
||
|
||
|
||
}
|
||
print("checked ${msg.proofs.length} proofs, $countInputs of them inputs");
|
||
|
||
print("sending blames");
|
||
send(Blames(blames: blames));
|
||
|
||
status = Tuple('running', 'awaiting restart');
|
||
|
||
// Await the final 'restartround' message. It might take some time
|
||
// to arrive since other players might be slow, and then the server
|
||
// itself needs to check blockchain.
|
||
await recv(['restartround'], timeout: Duration(seconds: 2 * (Protocol.STANDARD_TIMEOUT.round() + Protocol.BLAME_VERIFY_TIME.round())));
|
||
|
||
return true;
|
||
} // end of runRound() function.
|
||
|
||
|
||
|
||
|
||
} // END OF CLASS
|
||
|