2023-07-27 17:33:39 +00:00
|
|
|
|
import 'dart:async';
|
2023-07-26 18:07:30 +00:00
|
|
|
|
import 'dart:convert';
|
|
|
|
|
import 'dart:math';
|
|
|
|
|
import 'dart:typed_data';
|
2023-07-27 17:33:39 +00:00
|
|
|
|
|
|
|
|
|
import 'package:collection/collection.dart';
|
2023-07-26 18:07:30 +00:00
|
|
|
|
import 'package:crypto/crypto.dart';
|
2023-07-27 17:33:39 +00:00
|
|
|
|
import 'package:fixnum/fixnum.dart';
|
2023-07-26 18:07:30 +00:00
|
|
|
|
import "package:pointycastle/export.dart";
|
2023-07-27 17:33:39 +00:00
|
|
|
|
import 'package:protobuf/protobuf.dart';
|
2023-07-26 18:07:30 +00:00
|
|
|
|
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
import 'comms.dart';
|
|
|
|
|
import 'connection.dart';
|
|
|
|
|
import 'covert.dart';
|
|
|
|
|
import 'encrypt.dart';
|
|
|
|
|
import 'fusion.pb.dart';
|
|
|
|
|
import 'pedersen.dart';
|
|
|
|
|
import 'protocol.dart';
|
|
|
|
|
import 'socketwrapper.dart';
|
|
|
|
|
import 'util.dart';
|
|
|
|
|
import 'validation.dart';
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
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
|
2023-07-27 17:33:39 +00:00
|
|
|
|
final dynamic pedersenNonce; // replace dynamic with actual type
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
ComponentResult(this.commitment, this.counter, this.component, this.proof,
|
2023-07-26 18:07:30 +00:00
|
|
|
|
this.privateKey,
|
2023-07-27 17:33:39 +00:00
|
|
|
|
{this.pedersenAmount, this.pedersenNonce});
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Transaction {
|
|
|
|
|
List<Input> Inputs = [];
|
|
|
|
|
List<Output> Outputs = [];
|
|
|
|
|
|
|
|
|
|
Transaction();
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
static Tuple txFromComponents(
|
|
|
|
|
List<dynamic> allComponents, List<dynamic> sessionHash) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
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;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
List<dynamic> signatures = [];
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Input(
|
|
|
|
|
{required this.prevTxid,
|
|
|
|
|
required this.prevIndex,
|
|
|
|
|
required this.pubKey,
|
|
|
|
|
required this.amount});
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
int sizeOfInput() {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
assert(1 < pubKey.length &&
|
|
|
|
|
pubKey.length < 76); // need to assume regular push opcode
|
2023-07-26 18:07:30 +00:00
|
|
|
|
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(
|
2023-07-27 17:33:39 +00:00
|
|
|
|
prevTxid: inputComponent.prevTxid, // Make sure the types are matching
|
2023-07-26 18:07:30 +00:00
|
|
|
|
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;
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
int amount = 0;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
Output({required this.value, required this.addr});
|
|
|
|
|
|
|
|
|
|
int sizeOfOutput() {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
List<int> scriptpubkey = addr
|
|
|
|
|
.toScript(); // assuming addr.toScript() returns List<int> that represents the scriptpubkey
|
2023-07-26 18:07:30 +00:00
|
|
|
|
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 {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
List<Input> coins =
|
|
|
|
|
[]; //"coins" and "inputs" are often synonmous in the original python code.
|
|
|
|
|
List<Output> outputs = [];
|
2023-07-26 18:07:30 +00:00
|
|
|
|
bool server_connected_and_greeted = false;
|
|
|
|
|
bool stopping = false;
|
|
|
|
|
bool stopping_if_not_running = false;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
String stopReason = "";
|
|
|
|
|
String tor_host = "";
|
|
|
|
|
bool server_ssl = false;
|
|
|
|
|
String server_host = "cashfusion.stackwallet.com";
|
2023-07-26 18:07:30 +00:00
|
|
|
|
int server_port = 8787;
|
|
|
|
|
|
|
|
|
|
//String server_host = "fusion.servo.cash";
|
|
|
|
|
//int server_port = 8789;
|
|
|
|
|
|
|
|
|
|
int tor_port = 0;
|
|
|
|
|
int roundcount = 0;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
String txid = "";
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Tuple<String, String> status = Tuple("", "");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
Connection? connection;
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
int numComponents = 0;
|
|
|
|
|
double componentFeeRate = 0;
|
|
|
|
|
double minExcessFee = 0;
|
|
|
|
|
double maxExcessFee = 0;
|
|
|
|
|
List<int> availableTiers = [];
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
int maxOutputs = 0;
|
|
|
|
|
int safety_sum_in = 0;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
Map<int, int> safety_exess_fees = {};
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Map<int, List<int>> tierOutputs =
|
|
|
|
|
{}; // not sure if this should be using outputs class.
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
int inactiveTimeLimit = 0;
|
|
|
|
|
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;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Transaction tx = Transaction();
|
|
|
|
|
List<int> myComponentIndexes = [];
|
|
|
|
|
List<int> myCommitmentIndexes = [];
|
|
|
|
|
Set<int> badComponents = {};
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Fusion() {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
//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 {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
print("DEBUG FUSION 223...fusion run....");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
try {
|
|
|
|
|
try {
|
|
|
|
|
// Check compatibility - This was done in python version to see if fast libsec installed.
|
|
|
|
|
// For now , in dart, just pass this test.
|
|
|
|
|
;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
} on Exception catch (e) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
// 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.
|
2023-07-27 17:33:39 +00:00
|
|
|
|
if (tor_host.isNotEmpty &&
|
|
|
|
|
tor_port != 0 &&
|
|
|
|
|
!await isTorPort(tor_host, tor_port)) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
throw FusionError("Can't connect to Tor proxy at $tor_host:$tor_port");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Check stop condition
|
|
|
|
|
check_stop(running: false);
|
2023-07-27 17:33:39 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
print(e);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Check coins
|
|
|
|
|
check_coins();
|
2023-07-27 17:33:39 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
print(e);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Connect to server
|
|
|
|
|
status = Tuple("connecting", "");
|
|
|
|
|
try {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
connection = await openConnection(server_host, server_port,
|
|
|
|
|
connTimeout: 5.0, defaultTimeout: 5.0, ssl: server_ssl);
|
|
|
|
|
} catch (e) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
print("Connect failed: $e");
|
|
|
|
|
String sslstr = server_ssl ? ' SSL ' : '';
|
2023-07-27 17:33:39 +00:00
|
|
|
|
throw FusionError(
|
|
|
|
|
'Could not connect to $sslstr$server_host:$server_port');
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
2023-07-27 17:33:39 +00:00
|
|
|
|
} catch (e) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
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);
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
print("FUSION DEBUG 273");
|
|
|
|
|
print("RETURNING early in fusion_run....");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// launch the covert submitter
|
|
|
|
|
CovertSubmitter covert = await start_covert();
|
|
|
|
|
try {
|
|
|
|
|
// Pool started. Keep running rounds until fail or complete.
|
|
|
|
|
while (true) {
|
|
|
|
|
roundcount += 1;
|
|
|
|
|
if (await run_round(covert)) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
covert.stop();
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
(await connection)?.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2023-07-27 17:33:39 +00:00
|
|
|
|
} on FusionError catch (err) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
print('Failed: ${err}');
|
|
|
|
|
status.item1 = "failed";
|
2023-07-27 17:33:39 +00:00
|
|
|
|
status.item2 = err.toString(); // setting the error message
|
|
|
|
|
} catch (exc) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
print('Exception: ${exc}');
|
|
|
|
|
status.item1 = "failed";
|
2023-07-27 17:33:39 +00:00
|
|
|
|
status.item2 =
|
|
|
|
|
"Exception: ${exc.toString()}"; // setting the exception message
|
2023-07-26 18:07:30 +00:00
|
|
|
|
} 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-27 17:33:39 +00:00
|
|
|
|
} // end fusion_run function.
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
Future<CovertSubmitter> start_covert() async {
|
|
|
|
|
// Function implementation here...
|
|
|
|
|
|
|
|
|
|
// For now, just return a new instance of CovertSubmitter
|
2023-07-27 17:33:39 +00:00
|
|
|
|
return CovertSubmitter("dummy", 0, true, "some_host", 0, 0, 0, 0);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<bool> run_round(CovertSubmitter covert) async {
|
|
|
|
|
// function implementation here...
|
|
|
|
|
|
|
|
|
|
// placeholder return statement
|
|
|
|
|
return Future.value(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
void check_coins() {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
// Implement by calling wallet layer to check the coins are ok.
|
|
|
|
|
return;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
}
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
static void foo() {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
print("hello");
|
|
|
|
|
}
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
void clear_coins() {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
coins = [];
|
2023-07-27 17:33:39 +00:00
|
|
|
|
}
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
void addCoins(List<Input> newCoins) {
|
|
|
|
|
coins.addAll(newCoins);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void notify_coins_UI() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
static bool walletCanFuse() {
|
|
|
|
|
return true;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
// Implement logic here to return false if the wallet can't fuse. (If its read only or non P2PKH)
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
static double nextDoubleNonZero(Random rng) {
|
|
|
|
|
double value = 0.0;
|
|
|
|
|
while (value == 0.0) {
|
|
|
|
|
value = rng.nextDouble();
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
static List<int>? randomOutputsForTier(
|
|
|
|
|
Random rng, int inputAmount, int scale, int offset, int maxCount) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
if (inputAmount < offset) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
double lambd = 1.0 / scale;
|
|
|
|
|
int remaining = inputAmount;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
List<double> values = []; // list of fractional random values without offset
|
|
|
|
|
bool didBreak =
|
|
|
|
|
false; // Add this flag to detect when a break is encountered
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
for (int i = 0; i < maxCount + 1; i++) {
|
|
|
|
|
double val = -lambd * log(nextDoubleNonZero(rng));
|
|
|
|
|
remaining -= (val.ceil() + offset);
|
|
|
|
|
if (remaining < 0) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
didBreak = true; // If you break, set this flag to true
|
2023-07-26 18:07:30 +00:00
|
|
|
|
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();
|
2023-07-27 17:33:39 +00:00
|
|
|
|
assert(normedCumsum[normedCumsum.length - 1] == desiredRandomSum,
|
|
|
|
|
'Last element of normedCumsum is not equal to desiredRandomSum');
|
2023-07-26 18:07:30 +00:00
|
|
|
|
List<int> differences = [];
|
2023-07-27 17:33:39 +00:00
|
|
|
|
differences.add(normedCumsum[0]); // First element
|
2023-07-26 18:07:30 +00:00
|
|
|
|
for (int i = 1; i < normedCumsum.length; i++) {
|
|
|
|
|
differences.add(normedCumsum[i] - normedCumsum[i - 1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<int> result = differences.map((d) => offset + d).toList();
|
2023-07-27 17:33:39 +00:00
|
|
|
|
assert(result.reduce((a, b) => a + b) == inputAmount,
|
|
|
|
|
'Sum of result is not equal to inputAmount');
|
2023-07-26 18:07:30 +00:00
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
static List<ComponentResult> genComponents(
|
|
|
|
|
int numBlanks, List<Input> inputs, List<Output> outputs, int feerate) {
|
|
|
|
|
assert(numBlanks >= 0);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
List<Tuple<Component, int>> components = [];
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
// 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));
|
|
|
|
|
}
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
for (Output output in outputs) {
|
|
|
|
|
var script = output.addr.toScript();
|
|
|
|
|
int fee = Util.componentFee(output.sizeOfOutput(), feerate);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
var comp = Component();
|
|
|
|
|
comp.output =
|
|
|
|
|
OutputComponent(scriptpubkey: script, amount: Int64(output.value));
|
|
|
|
|
components.add(Tuple<Component, int>(comp, -output.value - fee));
|
|
|
|
|
}
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
for (int i = 0; i < numBlanks; i++) {
|
|
|
|
|
var comp = Component();
|
|
|
|
|
comp.blank = BlankComponent();
|
|
|
|
|
components.add(Tuple<Component, int>(comp, 0));
|
|
|
|
|
}
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
List<ComponentResult> resultList = [];
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
components.asMap().forEach((cnum, Tuple<Component, int> componentTuple) {
|
|
|
|
|
Uint8List salt = Util.tokenBytes(32);
|
|
|
|
|
componentTuple.item1.saltCommitment = Util.sha256(salt);
|
|
|
|
|
var compser = componentTuple.item1.writeToBuffer();
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Tuple<Uint8List, Uint8List> keyPair = Util.genKeypair();
|
|
|
|
|
Uint8List privateKey = keyPair.item1;
|
|
|
|
|
Uint8List pubKey = keyPair.item2;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Commitment commitmentInstance =
|
|
|
|
|
setup.commit(BigInt.from(componentTuple.item2));
|
|
|
|
|
Uint8List amountCommitment = commitmentInstance.PUncompressed;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
// Convert BigInt nonce to Uint8List
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Uint8List pedersenNonce = Uint8List.fromList(
|
|
|
|
|
[int.parse(commitmentInstance.nonce.toRadixString(16), radix: 16)]);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
// Generating initial commitment
|
2023-07-27 17:33:39 +00:00
|
|
|
|
InitialCommitment commitment = InitialCommitment(
|
|
|
|
|
saltedComponentHash:
|
|
|
|
|
Util.sha256(Uint8List.fromList([...compser, ...salt])),
|
|
|
|
|
amountCommitment: amountCommitment,
|
|
|
|
|
communicationKey: pubKey);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Uint8List commitser = commitment.writeToBuffer();
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
// Generating proof
|
|
|
|
|
Proof proof =
|
|
|
|
|
Proof(componentIdx: cnum, salt: salt, pedersenNonce: pedersenNonce);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
// Adding result to list
|
|
|
|
|
resultList
|
|
|
|
|
.add(ComponentResult(commitser, cnum, compser, proof, privateKey));
|
|
|
|
|
});
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
return resultList;
|
|
|
|
|
}
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Future<GeneratedMessage> recv2(
|
|
|
|
|
SocketWrapper socketwrapper, List<String> expectedMsgNames,
|
|
|
|
|
{Duration? timeout}) async {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
if (connection == null) {
|
|
|
|
|
throw FusionError('Connection not initialized');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = await recvPb2(
|
2023-07-27 17:33:39 +00:00
|
|
|
|
socketwrapper, connection!, ServerMessage, expectedMsgNames,
|
|
|
|
|
timeout: timeout);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
var submsg = result.item1;
|
|
|
|
|
var mtype = result.item2;
|
|
|
|
|
|
|
|
|
|
if (mtype == 'error') {
|
|
|
|
|
throw FusionError('server error: ${submsg.toString()}');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return submsg;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Future<GeneratedMessage> recv(List<String> expectedMsgNames,
|
|
|
|
|
{Duration? timeout}) async {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
// DEPRECATED
|
|
|
|
|
if (connection == null) {
|
|
|
|
|
throw FusionError('Connection not initialized');
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
var result = await recvPb(connection!, ServerMessage, expectedMsgNames,
|
|
|
|
|
timeout: timeout);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Future<void> send2(SocketWrapper socketwrapper, GeneratedMessage submsg,
|
|
|
|
|
{Duration? timeout}) async {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
if (connection != null) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
await sendPb2(socketwrapper, connection!, ClientMessage, submsg,
|
|
|
|
|
timeout: timeout);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
} 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());
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
ClientMessage clientMessage = ClientMessage()..clienthello = clientHello;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
//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();
|
|
|
|
|
*/
|
2023-07-27 17:33:39 +00:00
|
|
|
|
send2(socketwrapper, clientMessage);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
var replyMsg = await recv2(socketwrapper, ['serverhello']);
|
|
|
|
|
if (replyMsg is ServerMessage) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
ServerHello reply = replyMsg.serverhello;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
numComponents = reply.numComponents;
|
|
|
|
|
componentFeeRate = reply.componentFeerate.toDouble();
|
|
|
|
|
minExcessFee = reply.minExcessFee.toDouble();
|
|
|
|
|
maxExcessFee = reply.maxExcessFee.toDouble();
|
|
|
|
|
availableTiers = reply.tiers.map((tier) => tier.toInt()).toList();
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
// 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');
|
|
|
|
|
}
|
2023-07-26 18:07:30 +00:00
|
|
|
|
} else {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
throw Exception(
|
|
|
|
|
'Received unexpected message type: ${replyMsg.runtimeType}');
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> allocateOutputs(socketwrapper) async {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
print("DBUG allocateoutputs 746");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
print("CHECK socketwrapper 746");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
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) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
throw FusionError(
|
|
|
|
|
'Too few distinct inputs selected ($numDistinct); cannot satisfy output count constraint (>= $minOutputs, <= $maxOutputs)');
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int sumInputsValue = inputs.map((e) => e.value).reduce((a, b) => a + b);
|
2023-07-27 17:33:39 +00:00
|
|
|
|
int inputFees = inputs
|
|
|
|
|
.map(
|
|
|
|
|
(e) => Util.componentFee(e.sizeOfInput(), componentFeeRate.toInt()))
|
|
|
|
|
.reduce((a, b) => a + b);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
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));
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
print("DBUG allocateoutputs 785");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
tierOutputs = {};
|
|
|
|
|
var excessFees = <int, int>{};
|
|
|
|
|
for (var scale in availableTiers) {
|
|
|
|
|
int fuzzFeeMax = scale ~/ 1000000;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
int fuzzFeeMaxReduced = min(
|
|
|
|
|
fuzzFeeMax,
|
|
|
|
|
min(Protocol.MAX_EXCESS_FEE - minExcessFee.toInt(),
|
|
|
|
|
maxExcessFee.toInt()));
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
assert(fuzzFeeMaxReduced >= 0);
|
|
|
|
|
int fuzzFee = rng.nextInt(fuzzFeeMaxReduced + 1);
|
|
|
|
|
|
|
|
|
|
int reducedAvailForOutputs = availForOutputs - fuzzFee;
|
|
|
|
|
if (reducedAvailForOutputs < offsetPerOutput) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
var outputs = randomOutputsForTier(
|
|
|
|
|
rng, reducedAvailForOutputs, scale, offsetPerOutput, maxOutputs);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
if (outputs != null) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
print(outputs);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
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');
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
print("CHECK socketwrapper 839");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
socketwrapper.status();
|
|
|
|
|
safety_sum_in = sumInputsValue;
|
|
|
|
|
safety_exess_fees = excessFees;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> registerAndWait(SocketWrapper socketwrapper) async {
|
|
|
|
|
// 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) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
throw FusionError(
|
|
|
|
|
'No outputs available at any tier (selected inputs were too small / too large).');
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
print('registering for tiers: $tiersSorted');
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
int self_fuse = 1; // Temporary value for now
|
|
|
|
|
var cashfusionTag = [1]; // temp value for now
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
check_stop(running: false);
|
|
|
|
|
check_coins();
|
|
|
|
|
|
|
|
|
|
var tags = [JoinPools_PoolTag(id: cashfusionTag, limit: self_fuse)];
|
|
|
|
|
|
|
|
|
|
// Create JoinPools message
|
2023-07-27 17:33:39 +00:00
|
|
|
|
JoinPools joinPools =
|
|
|
|
|
JoinPools(tiers: tiersSorted.map((i) => Int64(i)).toList(), tags: tags);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
// Wrap it in a ClientMessage
|
2023-07-27 17:33:39 +00:00
|
|
|
|
ClientMessage clientMessage = ClientMessage()..joinpools = joinPools;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
send2(socketwrapper, clientMessage);
|
|
|
|
|
|
|
|
|
|
status = Tuple<String, String>('waiting', 'Registered for tiers');
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
var tiersStrings = {
|
|
|
|
|
for (var entry in tierOutputs.entries)
|
|
|
|
|
entry.key:
|
|
|
|
|
(entry.key * 1e-8).toStringAsFixed(8).replaceAll(RegExp(r'0+$'), '')
|
|
|
|
|
};
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
while (true) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
var msg = await recv2(socketwrapper, ['tierstatusupdate', 'fusionbegin'],
|
|
|
|
|
timeout: Duration(seconds: 10));
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
var fieldInfoFusionBegin = msg.info_.byName["fusionbegin"];
|
2023-07-27 17:33:39 +00:00
|
|
|
|
if (fieldInfoFusionBegin != null &&
|
|
|
|
|
msg.hasField(fieldInfoFusionBegin.tagNumber)) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
check_stop(running: false);
|
|
|
|
|
check_coins();
|
|
|
|
|
|
|
|
|
|
// Define the bool variable
|
|
|
|
|
|
|
|
|
|
var fieldInfo = msg.info_.byName["tierstatusupdate"];
|
|
|
|
|
if (fieldInfo == null) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
throw FusionError(
|
|
|
|
|
'Expected field not found in message: tierstatusupdate');
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool messageIsTierStatusUpdate = msg.hasField(fieldInfo.tagNumber);
|
|
|
|
|
|
|
|
|
|
if (!messageIsTierStatusUpdate) {
|
|
|
|
|
throw FusionError('Expected a TierStatusUpdate message');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
late var statuses;
|
|
|
|
|
if (messageIsTierStatusUpdate) {
|
|
|
|
|
//TierStatusUpdate tierStatusUpdate = msg.tierstatusupdate;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
var tierStatusUpdate =
|
|
|
|
|
msg.getField(fieldInfo.tagNumber) as TierStatusUpdate;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
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 / entry.value.min_players;
|
|
|
|
|
if (frac >= maxfraction) {
|
|
|
|
|
if (frac > maxfraction) {
|
|
|
|
|
maxfraction = frac;
|
|
|
|
|
maxtiers.clear();
|
|
|
|
|
}
|
|
|
|
|
maxtiers.add(entry.key);
|
|
|
|
|
}
|
|
|
|
|
if (entry.value.hasField('time_remaining')) {
|
|
|
|
|
int tr = entry.value.time_remaining;
|
|
|
|
|
if (besttime == null || tr < besttime) {
|
|
|
|
|
besttime = tr;
|
|
|
|
|
besttimetier = entry.key;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
throw FusionError(
|
|
|
|
|
'server reported status on tier we are not registered for');
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
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 (DateTime.now().millisecondsSinceEpoch > inactiveTimeLimit) {
|
|
|
|
|
throw FusionError('stopping due to inactivity');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (besttime != null) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
status = Tuple<String, String>(
|
|
|
|
|
'waiting', 'Starting in ${besttime}s. $tiersString');
|
2023-07-26 18:07:30 +00:00
|
|
|
|
} else if (maxfraction >= 1) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
status =
|
|
|
|
|
Tuple<String, String>('waiting', 'Starting soon. $tiersString');
|
2023-07-26 18:07:30 +00:00
|
|
|
|
} else if (displayBest.isNotEmpty || displayMid.isNotEmpty) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
status = Tuple<String, String>(
|
|
|
|
|
'waiting', '${(maxfraction * 100).round()}% full. $tiersString');
|
2023-07-26 18:07:30 +00:00
|
|
|
|
} else {
|
|
|
|
|
status = Tuple<String, String>('waiting', tiersString);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fieldInfoFusionBegin = msg.info_.byName["fusionbegin"];
|
|
|
|
|
if (fieldInfoFusionBegin == null) {
|
|
|
|
|
throw FusionError('Expected field not found in message: fusionbegin');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool messageIsFusionBegin = msg.hasField(fieldInfoFusionBegin.tagNumber);
|
|
|
|
|
if (!messageIsFusionBegin) {
|
|
|
|
|
throw FusionError('Expected a FusionBegin message');
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
t_fusionBegin = DateTime.now();
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
var clockMismatch =
|
|
|
|
|
msg.serverTime - DateTime.now().millisecondsSinceEpoch / 1000;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
if (clockMismatch.abs() > Protocol.MAX_CLOCK_DISCREPANCY) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
throw FusionError(
|
|
|
|
|
"Clock mismatch too large: ${clockMismatch.toStringAsFixed(3)}.");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tier = msg.tier;
|
|
|
|
|
if (msg is FusionBegin) {
|
|
|
|
|
covertDomainB = Uint8List.fromList(msg.covertDomain);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
covertPort = msg.covertPort;
|
|
|
|
|
covertSSL = msg.covertSSL;
|
|
|
|
|
beginTime = msg.serverTime;
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
lastHash = Util.calcInitialHash(
|
|
|
|
|
tier, covertDomainB, covertPort, covertSSL, beginTime);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
var outAmounts = tierOutputs[tier];
|
|
|
|
|
var outAddrs = Util.reserve_change_addresses(outAmounts?.length ?? 0);
|
|
|
|
|
|
|
|
|
|
reservedAddresses = outAddrs;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
outputs = Util.zip(outAmounts ?? [], outAddrs)
|
|
|
|
|
.map((pair) => Output(value: pair[0], addr: pair[1]))
|
|
|
|
|
.toList();
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
safetyExcessFee = safety_exess_fees[tier] ?? 0;
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
print(
|
|
|
|
|
"starting fusion rounds at tier $tier: ${coins.length} inputs and ${outputs.length} outputs");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<CovertSubmitter> startCovert() async {
|
|
|
|
|
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,
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Protocol.COVERT_SUBMIT_TIMEOUT);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
try {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
covert.scheduleConnections(t_fusionBegin,
|
2023-07-26 18:07:30 +00:00
|
|
|
|
Duration(seconds: Protocol.COVERT_CONNECT_WINDOW.toInt()),
|
|
|
|
|
numSpares: Protocol.COVERT_CONNECT_SPARES.toInt(),
|
2023-07-27 17:33:39 +00:00
|
|
|
|
connectTimeout: Protocol.COVERT_CONNECT_TIMEOUT.toInt());
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
// loop until a just a bit before we're expecting startRound, watching for status updates
|
2023-07-27 17:33:39 +00:00
|
|
|
|
final tend = t_fusionBegin.add(Duration(
|
|
|
|
|
seconds: (Protocol.WARMUP_TIME - Protocol.WARMUP_SLOP - 1).round()));
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
while (DateTime.now().millisecondsSinceEpoch / 1000 <
|
|
|
|
|
tend.millisecondsSinceEpoch / 1000) {
|
|
|
|
|
int numConnected =
|
|
|
|
|
covert.slots.where((s) => s.covConn?.connection != null).length;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
int numSpareConnected =
|
|
|
|
|
covert.spareConnections.where((c) => c.connection != null).length;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
status = Tuple('running',
|
|
|
|
|
'Setting up Tor connections ($numConnected+$numSpareConnected out of $numComponents)');
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
await Future.delayed(Duration(seconds: 1));
|
|
|
|
|
|
|
|
|
|
covert.checkOk();
|
|
|
|
|
this.check_stop();
|
|
|
|
|
this.check_coins();
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
covert.stop();
|
|
|
|
|
rethrow;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return covert;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void runRound(CovertSubmitter covert) async {
|
|
|
|
|
status = Tuple('running', 'Starting round ${roundcount.toString()}');
|
2023-07-27 17:33:39 +00:00
|
|
|
|
int timeoutInSeconds =
|
|
|
|
|
(2 * Protocol.WARMUP_SLOP + Protocol.STANDARD_TIMEOUT).toInt();
|
|
|
|
|
var msg = await recv(['startround'],
|
|
|
|
|
timeout: Duration(seconds: timeoutInSeconds));
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
// Record the time we got this message; it forms the basis time for all covert activities.
|
|
|
|
|
final covertT0 = DateTime.now().millisecondsSinceEpoch / 1000;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
double covertClock() =>
|
|
|
|
|
(DateTime.now().millisecondsSinceEpoch / 1000) - covertT0;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
final roundTime = (msg as StartRound).serverTime;
|
|
|
|
|
|
|
|
|
|
// Check the server's declared unix time, which will be committed.
|
2023-07-27 17:33:39 +00:00
|
|
|
|
final clockMismatch = (msg as StartRound).serverTime -
|
|
|
|
|
DateTime.now().millisecondsSinceEpoch / 1000;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
if (clockMismatch.abs() > Protocol.MAX_CLOCK_DISCREPANCY) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
throw FusionError(
|
|
|
|
|
"Clock mismatch too large: ${clockMismatch.toInt().toStringAsPrecision(3)}.");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (t_fusionBegin != null) {
|
|
|
|
|
// On the first startround message, check that the warmup time was within acceptable bounds.
|
2023-07-27 17:33:39 +00:00
|
|
|
|
final lag = covertT0 -
|
|
|
|
|
(t_fusionBegin.millisecondsSinceEpoch / 1000) -
|
|
|
|
|
Protocol.WARMUP_TIME;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
if (lag.abs() > Protocol.WARMUP_SLOP) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
throw FusionError(
|
|
|
|
|
"Warmup period too different from expectation (|${lag.toStringAsFixed(3)}s| > ${Protocol.WARMUP_SLOP.toStringAsFixed(3)}s).");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
t_fusionBegin = DateTime.now();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
print("round starting at ${DateTime.now().millisecondsSinceEpoch / 1000}");
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
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());
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
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)) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
throw Exception(
|
|
|
|
|
"(BUG!) Funds re-check failed -- aborting for safety. ${safeties.toString()}");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
final List<ComponentResult> genComponentsResults =
|
|
|
|
|
genComponents(numBlanks, coins, outputs, componentFeeRate.toInt());
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
final List<Uint8List> myCommitments = [];
|
|
|
|
|
final List<int> myComponentSlots = [];
|
|
|
|
|
final List<Uint8List> myComponents = [];
|
|
|
|
|
final List<Proof> myProofs = [];
|
|
|
|
|
final List<Uint8List> privKeys = [];
|
2023-07-27 17:33:39 +00:00
|
|
|
|
final List<dynamic> pedersenAmount =
|
|
|
|
|
[]; // replace dynamic with the actual type
|
|
|
|
|
final List<dynamic> pedersenNonce =
|
|
|
|
|
[]; // replace dynamic with the actual type
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2023-07-27 17:33:39 +00:00
|
|
|
|
assert(excessFee ==
|
|
|
|
|
pedersenAmount.reduce(
|
|
|
|
|
(a, b) => a + b)); // sanity check that we didn't mess up the above
|
2023-07-26 18:07:30 +00:00
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
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,
|
2023-07-27 17:33:39 +00:00
|
|
|
|
blindSigRequests:
|
|
|
|
|
blindSigRequests.map((r) => r.getRequest() as List<int>).toList(),
|
2023-07-26 18:07:30 +00:00
|
|
|
|
));
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
msg = await recv(['blindsigresponses'],
|
|
|
|
|
timeout: Duration(seconds: Protocol.T_START_COMPS.toInt()));
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
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,
|
2023-07-27 17:33:39 +00:00
|
|
|
|
(index) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
if (msg is BlindSigResponses) {
|
|
|
|
|
var typedMsg = msg as BlindSigResponses;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
return blindSigRequests[index]
|
|
|
|
|
.finalize(typedMsg.scalars[index], check: true);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
} 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],
|
2023-07-27 17:33:39 +00:00
|
|
|
|
component: myComponents[i]);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
if (messages.any((element) => element == null)) {
|
|
|
|
|
throw FusionError('Messages list includes null values.');
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
final targetDateTime = DateTime.fromMillisecondsSinceEpoch(
|
|
|
|
|
((covertT0 + Protocol.T_START_COMPS) * 1000).toInt());
|
2023-07-26 18:07:30 +00:00
|
|
|
|
covert.scheduleSubmissions(targetDateTime, messages);
|
|
|
|
|
|
|
|
|
|
// While submitting, we download the (large) full commitment list.
|
2023-07-27 17:33:39 +00:00
|
|
|
|
msg = await recv(['allcommitments'],
|
|
|
|
|
timeout: Duration(seconds: Protocol.T_START_SIGS.toInt()));
|
2023-07-26 18:07:30 +00:00
|
|
|
|
AllCommitments allCommitmentsMsg = msg as AllCommitments;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
List<InitialCommitment> allCommitments =
|
|
|
|
|
allCommitmentsMsg.initialCommitments.map((commitmentBytes) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
return InitialCommitment.fromBuffer(commitmentBytes);
|
|
|
|
|
}).toList();
|
|
|
|
|
|
|
|
|
|
// Quick check on the commitment list.
|
|
|
|
|
if (allCommitments.toSet().length != allCommitments.length) {
|
|
|
|
|
throw FusionError('Commitments list includes duplicates.');
|
|
|
|
|
}
|
|
|
|
|
try {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
List<Uint8List> allCommitmentsBytes = allCommitments
|
|
|
|
|
.map((commitment) => commitment.writeToBuffer())
|
|
|
|
|
.toList();
|
|
|
|
|
myCommitmentIndexes =
|
|
|
|
|
myCommitments.map((c) => allCommitmentsBytes.indexOf(c)).toList();
|
2023-07-26 18:07:30 +00:00
|
|
|
|
} 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:
|
2023-07-27 17:33:39 +00:00
|
|
|
|
msg = await recv(['sharecovertcomponents'],
|
|
|
|
|
timeout: Duration(seconds: Protocol.T_START_SIGS.toInt()));
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
ShareCovertComponents shareCovertComponentsMsg =
|
|
|
|
|
msg as ShareCovertComponents;
|
2023-07-26 18:07:30 +00:00
|
|
|
|
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 {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
myComponentIndexes = myComponents
|
|
|
|
|
.map((c) => allComponents
|
|
|
|
|
.indexWhere((element) => ListEquality().equals(element, c)))
|
|
|
|
|
.toList();
|
2023-07-26 18:07:30 +00:00
|
|
|
|
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.
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
List<List<int>> allCommitmentsBytes = allCommitments
|
|
|
|
|
.map((commitment) => commitment.writeToBuffer().toList())
|
|
|
|
|
.toList();
|
|
|
|
|
List<int> sessionHash = Util.calcRoundHash(lastHash, roundPubKey,
|
|
|
|
|
roundTime.toInt(), allCommitmentsBytes, allComponents);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
if (shareCovertComponentsMsg.sessionHash != null &&
|
|
|
|
|
!ListEquality()
|
|
|
|
|
.equals(shareCovertComponentsMsg.sessionHash, sessionHash)) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
throw FusionError('Session hash mismatch (bug!)');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!shareCovertComponentsMsg.skipSignatures) {
|
|
|
|
|
print("starting covert signature submission");
|
|
|
|
|
status = Tuple('running', 'covert submission: signatures');
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
if (allComponents.toSet().length != allComponents.length) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
throw FusionError('Server component list includes duplicates.');
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
var txInputIndices =
|
|
|
|
|
Transaction.txFromComponents(allComponents, sessionHash);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
Tuple txData = Transaction.txFromComponents(allComponents, sessionHash);
|
|
|
|
|
tx = txData.item1;
|
|
|
|
|
List<int> inputIndices = txData.item2;
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
List<CovertTransactionSignature?> covertTransactionSignatureMessages =
|
|
|
|
|
List<CovertTransactionSignature?>.filled(myComponents.length, null);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
var my_combined = List<Tuple<int, Input>>.generate(
|
|
|
|
|
inputIndices.length,
|
2023-07-27 17:33:39 +00:00
|
|
|
|
(index) => Tuple(inputIndices[index], tx.Inputs[index]),
|
2023-07-26 18:07:30 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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(
|
2023-07-27 17:33:39 +00:00
|
|
|
|
covertT0DateTime
|
|
|
|
|
.add(Duration(milliseconds: Protocol.T_START_SIGS.toInt())),
|
|
|
|
|
covertTransactionSignatureMessages);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
// wait for result
|
|
|
|
|
int timeoutMillis = (Protocol.T_EXPECTING_CONCLUSION -
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Protocol.TS_EXPECTING_COVERT_COMPONENTS)
|
|
|
|
|
.toInt();
|
2023-07-26 18:07:30 +00:00
|
|
|
|
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();
|
2023-07-27 17:33:39 +00:00
|
|
|
|
String sumInStr = Util.formatSatoshis(sumIn, numZeros: 8);
|
|
|
|
|
String feeStr = totalFee.toString();
|
2023-07-26 18:07:30 +00:00
|
|
|
|
String feeLoc = 'fee';
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
String label =
|
|
|
|
|
"CashFusion ${coins.length}⇢${outputs.length}, ${sumInStr} BCH (−${feeStr} sats ${feeLoc})";
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
Util.updateWalletLabel(txid, label);
|
2023-07-27 17:33:39 +00:00
|
|
|
|
} else {
|
|
|
|
|
badComponents = msg.badComponents.toSet();
|
2023-07-26 18:07:30 +00:00
|
|
|
|
if (badComponents.intersection(myComponentIndexes.toSet()).isNotEmpty) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
print(
|
|
|
|
|
"bad components: ${badComponents.toList()} mine: ${myComponentIndexes.toList()}");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
throw FusionError("server thinks one of my components is bad!");
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-27 17:33:39 +00:00
|
|
|
|
} else {
|
|
|
|
|
// skip_signatures True
|
|
|
|
|
Set<int> badComponents = Set<int>();
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ### 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 = [];
|
2023-07-27 17:33:39 +00:00
|
|
|
|
for (int i = 0; i < allCommitments.length; i++) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
if (!myCommitmentIndexes.contains(i)) {
|
|
|
|
|
othersCommitmentIdxes.add(i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
int N = othersCommitmentIdxes.length;
|
|
|
|
|
assert(N == allCommitments.length - myCommitments.length);
|
|
|
|
|
if (N == 0) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
throw FusionError(
|
|
|
|
|
"Fusion failed with only me as player -- I can only blame myself.");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// where should I send my proofs?
|
|
|
|
|
List<InitialCommitment> dstCommits = [];
|
2023-07-27 17:33:39 +00:00
|
|
|
|
for (int i = 0; i < myCommitments.length; i++) {
|
|
|
|
|
dstCommits.add(allCommitments[
|
|
|
|
|
othersCommitmentIdxes[Util.randPosition(randomNumber, N, i)]]);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// generate the encrypted proofs
|
|
|
|
|
List<String> encproofs = List<String>.filled(myCommitments.length, '');
|
|
|
|
|
|
|
|
|
|
ECDomainParameters params = ECDomainParameters('secp256k1');
|
2023-07-27 17:33:39 +00:00
|
|
|
|
for (int i = 0; i < dstCommits.length; i++) {
|
2023-07-26 18:07:30 +00:00
|
|
|
|
InitialCommitment msg = dstCommits[i];
|
|
|
|
|
Proof proof = myProofs[i];
|
|
|
|
|
proof.componentIdx = myComponentIndexes[i];
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
ECPoint? communicationKeyPointMaybe =
|
|
|
|
|
params.curve.decodePoint(Uint8List.fromList(msg.communicationKey));
|
2023-07-26 18:07:30 +00:00
|
|
|
|
if (communicationKeyPointMaybe == null) {
|
|
|
|
|
// handle the error case here, e.g., throw an exception or skip this iteration.
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
ECPoint communicationKeyPoint = communicationKeyPointMaybe;
|
|
|
|
|
|
|
|
|
|
try {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
Uint8List encryptedData = await encrypt(
|
|
|
|
|
proof.writeToBuffer(), communicationKeyPoint,
|
|
|
|
|
padToLength: 80);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
List<Uint8List> encodedEncproofs =
|
|
|
|
|
encproofs.map((e) => Uint8List.fromList(e.codeUnits)).toList();
|
|
|
|
|
this.send(MyProofsList(
|
|
|
|
|
encryptedProofs: encodedEncproofs, randomNumber: randomNumber));
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
status = Tuple('running', 'round failed - checking proofs');
|
|
|
|
|
|
|
|
|
|
print("receiving proofs");
|
2023-07-27 17:33:39 +00:00
|
|
|
|
msg = await this.recv(['theirproofslist'],
|
|
|
|
|
timeout: Duration(seconds: (2 * Protocol.STANDARD_TIMEOUT).round()));
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
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 {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
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
|
2023-07-26 18:07:30 +00:00
|
|
|
|
} on Exception catch (e) {
|
|
|
|
|
print("found an undecryptable proof");
|
2023-07-27 17:33:39 +00:00
|
|
|
|
blames.add(Blames_BlameProof(
|
|
|
|
|
whichProof: i, privkey: privKey, blameReason: 'undecryptable'));
|
2023-07-26 18:07:30 +00:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var commitment = InitialCommitment();
|
|
|
|
|
try {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
commitment
|
|
|
|
|
.mergeFromBuffer(commitmentBlob); // Method to parse protobuf data
|
2023-07-26 18:07:30 +00:00
|
|
|
|
} on FormatException catch (e) {
|
|
|
|
|
throw FusionError("Server relayed bad commitment");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var inpComp;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Convert allComponents to List<Uint8List>
|
2023-07-27 17:33:39 +00:00
|
|
|
|
List<Uint8List> allComponentsUint8 = allComponents
|
|
|
|
|
.map((component) => Uint8List.fromList(component))
|
|
|
|
|
.toList();
|
2023-07-26 18:07:30 +00:00
|
|
|
|
// Convert badComponents to List<int>
|
|
|
|
|
List<int> badComponentsList = badComponents.toList();
|
|
|
|
|
// Convert componentFeeRate to int if it's double
|
2023-07-27 17:33:39 +00:00
|
|
|
|
int componentFeerateInt = componentFeeRate
|
|
|
|
|
.round(); // or use .toInt() if you want to truncate instead of rounding
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
2023-07-27 17:33:39 +00:00
|
|
|
|
var inpComp = validateProofInternal(proofBlob, commitment,
|
|
|
|
|
allComponentsUint8, badComponentsList, componentFeerateInt);
|
2023-07-26 18:07:30 +00:00
|
|
|
|
} 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) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
print(
|
|
|
|
|
"found a bad input [${rp.srcCommitmentIdx}]: $e (${inpComp.prevTxid.reversed.toList().toHex()}:${inpComp.prevIndex})");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
|
|
|
|
|
var blameProof = Blames_BlameProof();
|
|
|
|
|
blameProof.whichProof = i;
|
|
|
|
|
blameProof.sessionKey = sKey;
|
2023-07-27 17:33:39 +00:00
|
|
|
|
blameProof.blameReason =
|
|
|
|
|
'input does not match blockchain: ' + e.toString();
|
2023-07-26 18:07:30 +00:00
|
|
|
|
blameProof.needLookupBlockchain = true;
|
|
|
|
|
blames.add(blameProof);
|
|
|
|
|
} catch (e) {
|
2023-07-27 17:33:39 +00:00
|
|
|
|
print(
|
|
|
|
|
"verified an input internally, but was unable to check it against blockchain: ${e}");
|
2023-07-26 18:07:30 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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.
|
2023-07-27 17:33:39 +00:00
|
|
|
|
await recv(['restartround'],
|
|
|
|
|
timeout: Duration(
|
|
|
|
|
seconds: 2 *
|
|
|
|
|
(Protocol.STANDARD_TIMEOUT.round() +
|
|
|
|
|
Protocol.BLAME_VERIFY_TIME.round())));
|
|
|
|
|
} // end of run_round() function.
|
2023-07-26 18:07:30 +00:00
|
|
|
|
} // END OF CLASS
|