package network.thunder.core.communication.processor.implementations.lnpayment;
import com.google.common.base.Preconditions;
import network.thunder.core.communication.objects.lightning.subobjects.ChannelStatus;
import network.thunder.core.communication.objects.lightning.subobjects.PaymentData;
import network.thunder.core.communication.objects.messages.impl.message.lnpayment.*;
import network.thunder.core.communication.objects.messages.interfaces.factories.LNPaymentMessageFactory;
import network.thunder.core.communication.objects.messages.interfaces.message.lnpayment.LNPayment;
import network.thunder.core.communication.processor.exceptions.LNPaymentException;
import network.thunder.core.communication.processor.interfaces.lnpayment.LNPaymentLogic;
import network.thunder.core.database.DBHandler;
import network.thunder.core.database.objects.Channel;
import network.thunder.core.etc.Constants;
import network.thunder.core.etc.ScriptTools;
import network.thunder.core.etc.Tools;
import network.thunder.core.lightning.RevocationHash;
import network.thunder.core.mesh.LNConfiguration;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.Script;
import java.util.ArrayList;
import java.util.List;
public class LNPaymentLogicImpl implements LNPaymentLogic {
DBHandler dbHandler;
Channel channel;
ChannelStatus statusTemp;
TransactionSignature signature1;
TransactionSignature signature2;
List<TransactionSignature> paymentSignatures = new ArrayList<>();
RevocationHash revocationHashClient;
RevocationHash revocationHashServer;
LNConfiguration configuration = new LNConfiguration();
LNPaymentMessageFactory messageFactory;
public LNPaymentLogicImpl (LNPaymentMessageFactory messageFactory, DBHandler dbHandler) {
this.messageFactory = messageFactory;
this.dbHandler = dbHandler;
}
@Override
public void initialise (Channel channel) {
this.channel = channel;
}
public Transaction getClientTransaction () {
Preconditions.checkNotNull(channel);
Transaction transaction = new Transaction(Constants.getNetwork());
transaction.addInput(channel.anchorTxHashClient, 0, Tools.getDummyScript());
transaction.addInput(channel.anchorTxHashServer, 0, Tools.getDummyScript());
transaction.addOutput(Coin.valueOf(0), ScriptTools.getChannelTxOutputRevocation(revocationHashClient,
channel.keyClient, channel.keyServer, Constants.ESCAPE_REVOCATION_TIME));
transaction.addOutput(Coin.valueOf(statusTemp.amountServer), ScriptTools.getChannelTxOutputPlain(channel.keyServer));
transaction = addPayments(transaction, statusTemp.getCloneReversed(), revocationHashClient, channel.keyClient, channel.keyServer);
long amountEncumbered = statusTemp.amountClient - transaction.getMessageSize() * statusTemp.feePerByte;
transaction.getOutput(0).setValue(Coin.valueOf(amountEncumbered));
return transaction;
}
public Transaction getServerTransaction () {
Preconditions.checkNotNull(channel);
Transaction transaction = new Transaction(Constants.getNetwork());
transaction.addInput(channel.anchorTxHashServer, 0, Tools.getDummyScript());
transaction.addInput(channel.anchorTxHashClient, 0, Tools.getDummyScript());
transaction.addOutput(Coin.valueOf(0), ScriptTools.getChannelTxOutputRevocation(revocationHashServer,
channel.keyServer, channel.keyClient, Constants.ESCAPE_REVOCATION_TIME));
transaction.addOutput(Coin.valueOf(statusTemp.amountClient), ScriptTools.getChannelTxOutputPlain(channel.keyClient));
transaction = addPayments(transaction, statusTemp, revocationHashServer, channel.keyServer, channel.keyClient);
long amountEncumbered = statusTemp.amountServer - transaction.getMessageSize() * statusTemp.feePerByte;
transaction.getOutput(0).setValue(Coin.valueOf(amountEncumbered));
return transaction;
}
public List<Transaction> getClientPaymentTransactions () {
Transaction t = getClientTransaction();
return getPaymentTransactions(t.getHash(), statusTemp.getCloneReversed(), revocationHashClient, channel.keyClient, channel.keyServer);
}
public List<Transaction> getServerPaymentTransactions () {
Transaction t = getServerTransaction();
return getPaymentTransactions(t.getHash(), statusTemp, revocationHashServer, channel.keyServer, channel.keyClient);
}
public List<TransactionSignature> getChannelSignatures () {
//The channelTransaction is finished, we just need to produce the signatures..
TransactionSignature signature1 = Tools.getSignature(getClientTransaction(), 0, channel.getScriptAnchorOutputClient().getProgram(), channel
.getKeyServer());
TransactionSignature signature2 = Tools.getSignature(getClientTransaction(), 1, channel.getScriptAnchorOutputServer().getProgram(), channel
.getKeyServer());
List<TransactionSignature> channelSignatures = new ArrayList<>();
channelSignatures.add(signature1);
channelSignatures.add(signature2);
return channelSignatures;
}
public List<TransactionSignature> getPaymentSignatures () {
List<Transaction> paymentTransactions = getClientPaymentTransactions();
List<TransactionSignature> signatureList = new ArrayList<>();
int index = 2;
for (Transaction t : paymentTransactions) {
TransactionSignature sig = Tools.getSignature(t, 0, getClientTransaction().getOutput(index).getScriptBytes(), channel.getKeyServer());
signatureList.add(sig);
index++;
}
return signatureList;
}
private Transaction addPayments (Transaction transaction, ChannelStatus channelStatus, RevocationHash revocationHash, ECKey keyServer, ECKey keyClient) {
List<PaymentData> allPayments = new ArrayList<>(channelStatus.remainingPayments);
allPayments.addAll(channelStatus.newPayments);
for (PaymentData payment : allPayments) {
//TODO value here should include fees for the next transaction you need to spend it..pa
Coin value = Coin.valueOf(payment.amount);
Script script;
if (payment.sending) {
script = ScriptTools.getChannelTxOutputPaymentSending(keyServer, keyClient, revocationHash, payment.secret, payment.timestampRefund);
} else {
script = ScriptTools.getChannelTxOutputPaymentReceiving(keyServer, keyClient, revocationHash, payment.secret, payment.timestampRefund);
}
transaction.addOutput(value, script);
}
return transaction;
}
private List<Transaction> getPaymentTransactions (Sha256Hash parentTransactionHash, ChannelStatus channelStatus, RevocationHash
revocationHash, ECKey keyServer, ECKey keyClient) {
List<PaymentData> allPayments = new ArrayList<>(channelStatus.remainingPayments);
allPayments.addAll(channelStatus.newPayments);
List<Transaction> transactions = new ArrayList<>(allPayments.size());
int index = 2;
for (PaymentData payment : allPayments) {
Transaction transaction = new Transaction(Constants.getNetwork());
transaction.addInput(parentTransactionHash, index, Tools.getDummyScript());
Coin value = Coin.valueOf(payment.amount);
Script script = ScriptTools.getPaymentTxOutput(keyServer, keyClient, revocationHash, payment.csvDelay);
transaction.addOutput(value, script);
transactions.add(transaction);
index++;
}
return transactions;
}
@Override
public void checkMessageIncoming (LNPayment message) {
Preconditions.checkNotNull(channel);
if (message instanceof LNPaymentAMessage) {
parseAMessage((LNPaymentAMessage) message);
} else if (message instanceof LNPaymentBMessage) {
parseBMessage((LNPaymentBMessage) message);
} else if (message instanceof LNPaymentCMessage) {
parseCMessage((LNPaymentCMessage) message);
} else if (message instanceof LNPaymentDMessage) {
parseDMessage((LNPaymentDMessage) message);
}
}
@Override
public Channel updateChannel (Channel channel) {
channel.channelSignature1 = this.signature1;
channel.channelSignature2 = this.signature2;
channel.paymentSignatures = this.paymentSignatures;
return channel;
}
@Override
public ChannelStatus getTemporaryChannelStatus () {
Preconditions.checkNotNull(channel);
return statusTemp.getClone();
}
@Override
public LNPaymentAMessage getAMessage (ChannelStatus newStatus) {
this.statusTemp = newStatus;
LNPaymentAMessage message = messageFactory.getMessageA(channel, statusTemp);
this.revocationHashServer = message.newRevocation;
return message;
}
@Override
public LNPaymentBMessage getBMessage () {
LNPaymentBMessage message = messageFactory.getMessageB(channel);
this.revocationHashServer = message.newRevocation;
return message;
}
@Override
public LNPaymentCMessage getCMessage () {
LNPaymentCMessage message = messageFactory.getMessageC(channel, getChannelSignatures(), getPaymentSignatures());
return message;
}
@Override
public LNPaymentDMessage getDMessage () {
LNPaymentDMessage message = messageFactory.getMessageD(channel);
return message;
}
private void parseAMessage (LNPaymentAMessage message) {
//We can have a lot of operations here, like adding/removing payments. We need to verify if they are correct.
long amountServer = channel.channelStatus.amountServer;
long amountClient = channel.channelStatus.amountClient;
revocationHashClient = null;
revocationHashServer = null;
statusTemp = message.channelStatus.getCloneReversed();
System.out.println("Check received new status: " + statusTemp);
System.out.println("Old status: " + channel.channelStatus);
checkPaymentsInNewStatus(channel.channelStatus, statusTemp);
checkRefundedPayments(statusTemp);
checkRedeemedPayments(statusTemp);
for (PaymentData refund : statusTemp.refundedPayments) {
amountServer += refund.amount;
}
for (PaymentData redeem : statusTemp.redeemedPayments) {
amountClient += redeem.amount;
}
for (PaymentData payment : statusTemp.newPayments) {
amountClient -= payment.amount;
}
if (amountClient != statusTemp.amountClient || amountClient < 0) {
throw new LNPaymentException("amountClient not correct.. Is " + statusTemp.amountClient + " Should be: " + amountClient);
}
if (amountServer != statusTemp.amountServer || amountServer < 0) {
throw new LNPaymentException("amountServer not correct..Is " + statusTemp.amountServer + " Should be: " + amountServer);
}
//Sufficient to test new payments
for (PaymentData payment : statusTemp.newPayments) {
int diff = Math.abs(Tools.currentTime() - payment.timestampOpen);
if (diff > configuration.MAX_DIFF_TIMESTAMPS) {
throw new LNPaymentException("timestampOpen is too far off. Calibrate your system clock. Diff: " + diff);
}
if (payment.csvDelay < configuration.MIN_REVOCATION_DELAY || payment.csvDelay > configuration.MAX_REVOCATION_DELAY) {
throw new LNPaymentException("Payment-Revocation delay not within allowed boundaries. Is: " + payment.csvDelay);
}
diff = payment.timestampRefund - payment.timestampOpen;
if (diff > configuration.MAX_OVERLAY_REFUND * configuration.MAX_REFUND_DELAY * OnionObject.MAX_HOPS) {
throw new LNPaymentException("Refund timeout is too large. Is: " + diff);
}
//TODO Think about how we can solve guessing here, about us being the final receiver..
if (diff < configuration.MIN_OVERLAY_REFUND * configuration.MIN_REFUND_DELAY) {
throw new LNPaymentException("Refund timeout is too short. Is: " + diff);
}
}
if (statusTemp.csvDelay < configuration.MIN_REVOCATION_DELAY || statusTemp.csvDelay > configuration.MAX_REVOCATION_DELAY) {
throw new LNPaymentException("Change-Revocation delay not within allowed boundaries. Is: " + statusTemp.csvDelay);
}
if (statusTemp.feePerByte > configuration.MAX_FEE_PER_BYTE || statusTemp.feePerByte < configuration.MIN_FEE_PER_BYTE) {
throw new LNPaymentException("feePerByte not within allowed boundaries. Is: " + statusTemp.feePerByte);
}
dbHandler.insertRevocationHash(message.newRevocation);
revocationHashClient = message.newRevocation;
}
private void parseBMessage (LNPaymentBMessage message) {
if (!message.success) {
throw new LNPaymentException(message.error);
}
revocationHashClient = message.newRevocation;
dbHandler.insertRevocationHash(message.newRevocation);
}
private void parseCMessage (LNPaymentCMessage message) {
paymentSignatures.clear();
signature1 = TransactionSignature.decodeFromBitcoin(message.newCommitSignature1, true);
signature2 = TransactionSignature.decodeFromBitcoin(message.newCommitSignature2, true);
Transaction channelTransaction = getServerTransaction();
Sha256Hash hash1 = channelTransaction.hashForSignature(0, channel.getScriptAnchorOutputServer(), Transaction.SigHash.ALL, false);
Sha256Hash hash2 = channelTransaction.hashForSignature(1, channel.getScriptAnchorOutputClient(), Transaction.SigHash.ALL, false);
if (!channel.keyClient.verify(hash1, signature1)) {
throw new LNPaymentException("Signature1 is not correct..");
}
if (!channel.keyClient.verify(hash2, signature2)) {
throw new LNPaymentException("Signature2 is not correct..");
}
List<PaymentData> allPayments = new ArrayList<>(statusTemp.remainingPayments);
allPayments.addAll(statusTemp.newPayments);
List<Transaction> paymentTransactions = getServerPaymentTransactions();
if (allPayments.size() != message.newPaymentSignatures.size()) {
throw new LNPaymentException("Size of payment signature list is incorrect");
}
for (int i = 0; i < allPayments.size(); ++i) {
Transaction transaction = paymentTransactions.get(i);
PaymentData payment = allPayments.get(i);
TransactionSignature signature = TransactionSignature.decodeFromBitcoin(message.newPaymentSignatures.get(i), true);
Script scriptPubKey = channelTransaction.getOutput(i + 2).getScriptPubKey();
Sha256Hash hash = transaction.hashForSignature(0, scriptPubKey, Transaction.SigHash.ALL, false);
if (!channel.keyClient.verify(hash, signature)) {
throw new LNPaymentException("Payment Signature " + i + " is not correct..");
}
paymentSignatures.add(signature);
}
}
private void parseDMessage (LNPaymentDMessage message) {
for (RevocationHash hash : message.oldRevocationHashes) {
if (!hash.check()) {
throw new LNPaymentException("hash.check() returned false");
}
}
if (!dbHandler.checkOldRevocationHashes(message.oldRevocationHashes)) {
throw new LNPaymentException("Could not verify all old revocation hashes..");
}
}
private void saveSignatures () {
}
private void checkPaymentsInNewStatus (ChannelStatus oldStatus, ChannelStatus newStatus) {
oldStatus = oldStatus.getClone();
newStatus = newStatus.getClone();
for (PaymentData paymentData : oldStatus.remainingPayments) {
//All of these should be in either refund/settled/old
if (newStatus.remainingPayments.remove(paymentData)) {
continue;
}
if (newStatus.redeemedPayments.remove(paymentData)) {
continue;
}
if (newStatus.refundedPayments.remove(paymentData)) {
continue;
}
throw new LNPaymentException("Old payment that is in neither refund/settle/old");
}
if (newStatus.remainingPayments.size() + newStatus.refundedPayments.size() + newStatus.redeemedPayments.size() > 0) {
throw new LNPaymentException("New payments that are not in newPayments");
}
}
private void checkRefundedPayments (ChannelStatus newStatus) {
for (PaymentData paymentData : newStatus.refundedPayments) {
//We reversed the ChannelStatus, so if we are receiving, he is sending
if (!paymentData.sending) {
throw new LNPaymentException("Trying to refund a sent payment");
}
}
}
private void checkRedeemedPayments (ChannelStatus newStatus) {
for (PaymentData paymentData : newStatus.redeemedPayments) {
if (!paymentData.sending) {
throw new LNPaymentException("Trying to redeem a sent payment?");
}
if (!paymentData.secret.verify()) {
throw new LNPaymentException("Trying to redeem but failed to verify secret.");
}
}
}
}