diff --git a/dlc/contract.go b/dlc/contract.go index cc786cbdf..411391a2f 100644 --- a/dlc/contract.go +++ b/dlc/contract.go @@ -7,6 +7,7 @@ import ( ) const COINTYPE_NOT_SET = ^uint32(0) // Max Uint +const FEEPERBYTE_NOT_SET = ^uint32(0) // Max Uint // AddContract starts a new draft contract func (mgr *DlcManager) AddContract() (*lnutil.DlcContract, error) { @@ -244,3 +245,23 @@ func (mgr *DlcManager) SetContractCoinType(cIdx uint64, cointype uint32) error { return nil } + + +//SetContractFeePerByte sets the fee per byte for a particular contract +func (mgr *DlcManager) SetContractFeePerByte(cIdx uint64, feeperbyte uint32) error { + c, err := mgr.LoadContract(cIdx) + if err != nil { + return err + } + + if c.Status != lnutil.ContractStatusDraft { + return fmt.Errorf("You cannot change or set the coin type unless" + + " the contract is in Draft state") + } + + c.FeePerByte = feeperbyte + + mgr.SaveContract(c) + + return nil +} diff --git a/litrpc/dlccmds.go b/litrpc/dlccmds.go index bb6d84cb2..f54d0e625 100644 --- a/litrpc/dlccmds.go +++ b/litrpc/dlccmds.go @@ -314,6 +314,34 @@ func (r *LitRPC) SetContractCoinType(args SetContractCoinTypeArgs, return nil } + +type SetContractFeePerByteArgs struct { + CIdx uint64 + FeePerByte uint32 +} + +type SetContractFeePerByteReply struct { + Success bool +} + +// SetContractFeePerByte sets the coin type the contract will be in. Note that a +// peer that doesn't have a wallet of that type will automatically decline the +// contract. +func (r *LitRPC) SetContractFeePerByte(args SetContractFeePerByteArgs, + reply *SetContractFeePerByteReply) error { + var err error + + err = r.Node.DlcManager.SetContractFeePerByte(args.CIdx, args.FeePerByte) + if err != nil { + return err + } + + reply.Success = true + return nil +} + + + type OfferContractArgs struct { CIdx uint64 PeerIdx uint32 diff --git a/lnutil/dlclib.go b/lnutil/dlclib.go index a45c6724a..22de32345 100644 --- a/lnutil/dlclib.go +++ b/lnutil/dlclib.go @@ -6,9 +6,10 @@ import ( "encoding/binary" "fmt" "math/big" + "errors" "github.com/mit-dci/lit/btcutil/chaincfg/chainhash" - "github.com/mit-dci/lit/consts" + "github.com/mit-dci/lit/crypto/koblitz" "github.com/mit-dci/lit/logging" "github.com/mit-dci/lit/wire" @@ -48,6 +49,8 @@ type DlcContract struct { PeerIdx uint32 // Coin type CoinType uint32 + // Fee per byte + FeePerByte uint32 // Pub keys of the oracle and the R point used in the contract OracleA, OracleR [33]byte // The time we expect the oracle to publish @@ -125,6 +128,14 @@ func DlcContractFromBytes(b []byte) (*DlcContract, error) { return nil, err } c.CoinType = uint32(coinType) + + feePerByte, err := wire.ReadVarInt(buf, 0) + if err != nil { + logging.Errorf("Error while deserializing varint for coinType: %s", err.Error()) + return nil, err + } + c.FeePerByte = uint32(feePerByte) + c.OracleTimestamp, err = wire.ReadVarInt(buf, 0) if err != nil { return nil, err @@ -245,6 +256,7 @@ func (self *DlcContract) Bytes() []byte { buf.Write(self.OracleR[:]) wire.WriteVarInt(&buf, 0, uint64(self.PeerIdx)) wire.WriteVarInt(&buf, 0, uint64(self.CoinType)) + wire.WriteVarInt(&buf, 0, uint64(self.FeePerByte)) wire.WriteVarInt(&buf, 0, uint64(self.OracleTimestamp)) wire.WriteVarInt(&buf, 0, uint64(self.OurFundingAmount)) wire.WriteVarInt(&buf, 0, uint64(self.TheirFundingAmount)) @@ -441,37 +453,109 @@ func computePubKey(pubA, pubR [33]byte, msg []byte) ([33]byte, error) { func SettlementTx(c *DlcContract, d DlcContractDivision, ours bool) (*wire.MsgTx, error) { + + + // Maximum possible size of transaction here is + // Version 4 bytes + LockTime 4 bytes + Serialized varint size for the + // number of transaction inputs and outputs. + // n := 8 + VarIntSerializeSize(uint64(len(msg.TxIn))) + + // VarIntSerializeSize(uint64(len(msg.TxOut))) + + // Plus Witness Data 218 + // Plus Single input 41 + // Plus Their output 43 + // Plus Our output 31 + // Plus 2 for all wittness transactions + // Total max size of tx here is: 4 + 4 + 1 + 1 + 2 + 218 + 41 + 43 + 31 = 345 + // Vsize: ( (345 - 218 - 2) * 3 + 345 ) / 4 = 180 + + maxVsize := 180 + tx := wire.NewMsgTx() // set version 2, for op_csv tx.Version = 2 tx.AddTxIn(wire.NewTxIn(&c.FundingOutpoint, nil, nil)) - totalFee := int64(consts.DlcSettlementTxFee) // TODO: Calculate - feeEach := int64(float64(totalFee) / float64(2)) + + totalFee := uint32(maxVsize * int(c.FeePerByte)) + + feeEach := uint32(totalFee / uint32(2)) feeOurs := feeEach feeTheirs := feeEach + + totalContractValue := c.TheirFundingAmount + c.OurFundingAmount + valueOurs := d.ValueOurs + valueTheirs := totalContractValue - d.ValueOurs + + + if totalContractValue < int64(totalFee) { + return nil, errors.New("totalContractValue < totalFee") + } + + + vsize :=uint32(0) + // We don't have enough to pay for a fee. We get 0, our contract partner // pays the rest of the fee - if valueOurs < feeEach { - feeOurs = valueOurs - valueOurs = 0 - } else { - valueOurs = d.ValueOurs - feeOurs + if valueOurs < int64(feeOurs) { + + // Just recalculate totalFee, feeOurs, feeTheirs to exclude one of the output. + if ours { + + // exclude wire.NewTxOut from size (i.e 31) + vsize = uint32(149) + totalFee = vsize * uint32(c.FeePerByte) + + }else{ + + // exclude DlcOutput from size (i.e 43) + vsize = uint32(137) + totalFee = vsize * uint32(c.FeePerByte) + + } + + feeEach = uint32(float64(totalFee) / float64(2)) + feeOurs = feeEach + feeTheirs = feeEach + + if valueOurs == 0 { // Also if we win 0, our contract partner pays the totalFee + feeTheirs = totalFee + }else{ + + feeTheirs += uint32(valueOurs) + valueOurs = 0 + + } } - totalContractValue := c.TheirFundingAmount + c.OurFundingAmount - valueTheirs := totalContractValue - d.ValueOurs - if valueTheirs < feeEach { - feeTheirs = valueTheirs - valueTheirs = 0 - feeOurs = totalFee - feeTheirs - valueOurs = d.ValueOurs - feeOurs - } else { - valueTheirs -= feeTheirs + // Due to check above it is impossible (valueTheirs < feeTheirs) and + // (valueOurs < feeOurs) are satisfied at the same time. + if valueTheirs < int64(feeTheirs) { + if ours { + vsize = uint32(137) + }else{ + vsize = uint32(149) + } + totalFee = vsize * c.FeePerByte + feeEach = uint32(float64(totalFee) / float64(2)) + feeOurs = feeEach + feeTheirs = feeEach + + if valueTheirs == 0 { + feeOurs = totalFee + }else{ + feeOurs += uint32(valueTheirs) + valueTheirs = 0 + + } } + valueOurs -= int64(feeOurs) + valueTheirs -= int64(feeTheirs) + + var buf bytes.Buffer binary.Write(&buf, binary.BigEndian, uint64(0)) binary.Write(&buf, binary.BigEndian, uint64(0)) diff --git a/qln/dlc.go b/qln/dlc.go index fd61c7846..45f3cbaca 100644 --- a/qln/dlc.go +++ b/qln/dlc.go @@ -57,6 +57,10 @@ func (nd *LitNode) OfferDlc(peerIdx uint32, cIdx uint64) error { return fmt.Errorf("You need to set a coin type for the contract before offering it") } + if c.FeePerByte == dlc.FEEPERBYTE_NOT_SET { + return fmt.Errorf("You need to set a fee per byte for the contract before offering it") + } + if c.Division == nil { return fmt.Errorf("You need to set a payout division for the contract before offering it") } @@ -236,6 +240,7 @@ func (nd *LitNode) DlcOfferHandler(msg lnutil.DlcOfferMsg, peer *RemotePeer) { // Copy c.CoinType = msg.Contract.CoinType + c.FeePerByte = msg.Contract.FeePerByte c.OracleA = msg.Contract.OracleA c.OracleR = msg.Contract.OracleR c.OracleTimestamp = msg.Contract.OracleTimestamp @@ -474,18 +479,67 @@ func (nd *LitNode) BuildDlcFundingTransaction(c *lnutil.DlcContract) (wire.MsgTx var ourInputTotal int64 var theirInputTotal int64 + + our_txin_num := 0 for _, u := range c.OurFundingInputs { - tx.AddTxIn(wire.NewTxIn(&u.Outpoint, nil, nil)) + txin := wire.NewTxIn(&u.Outpoint, nil, nil) + + tx.AddTxIn(txin) ourInputTotal += u.Value + + our_txin_num += 1 + } + + + their_txin_num := 0 for _, u := range c.TheirFundingInputs { - tx.AddTxIn(wire.NewTxIn(&u.Outpoint, nil, nil)) + txin := wire.NewTxIn(&u.Outpoint, nil, nil) + + + tx.AddTxIn(txin) theirInputTotal += u.Value + + their_txin_num += 1 + } + + + //==================================================== + + // Here can be a situation when peers have different number of inputs. + // Therefore we have to calculate fees for each peer separately. + + // This transaction always will have 3 outputs ( 43 + 31 + 31) + tx_basesize := 10 + 43 + 31 + 31 + tx_size_foreach := tx_basesize / 2 + tx_size_foreach += 1 // rounding + + input_wit_size := 107 + + our_tx_vsize := uint32(((tx_size_foreach + (41 * our_txin_num)) * 3 + (tx_size_foreach + (41 * our_txin_num) + (input_wit_size*our_txin_num) )) / 4) + their_tx_vsize := uint32(((tx_size_foreach + (41 * their_txin_num)) * 3 + (tx_size_foreach + (41 * their_txin_num) + (input_wit_size*their_txin_num) )) / 4) + + //rounding + our_tx_vsize += 1 + their_tx_vsize += 1 + + + our_fee := int64(our_tx_vsize * c.FeePerByte) + their_fee := int64(their_tx_vsize * c.FeePerByte) + + // add change and sort - tx.AddTxOut(wire.NewTxOut(theirInputTotal-c.TheirFundingAmount-500, lnutil.DirectWPKHScriptFromPKH(c.TheirChangePKH))) - tx.AddTxOut(wire.NewTxOut(ourInputTotal-c.OurFundingAmount-500, lnutil.DirectWPKHScriptFromPKH(c.OurChangePKH))) + + their_txout := wire.NewTxOut(theirInputTotal-c.TheirFundingAmount-their_fee, lnutil.DirectWPKHScriptFromPKH(c.TheirChangePKH)) + tx.AddTxOut(their_txout) + + + our_txout := wire.NewTxOut(ourInputTotal-c.OurFundingAmount-our_fee, lnutil.DirectWPKHScriptFromPKH(c.OurChangePKH)) + tx.AddTxOut(our_txout) + + txsort.InPlaceSort(tx) @@ -609,46 +663,77 @@ func (nd *LitNode) SettleContract(cIdx uint64, oracleValue int64, oracleSig [32] return [32]byte{}, [32]byte{}, err } - // TODO: Claim the contract settlement output back to our wallet - otherwise the peer can claim it after locktime. - txClaim := wire.NewMsgTx() - txClaim.Version = 2 - settleOutpoint := wire.OutPoint{Hash: settleTx.TxHash(), Index: 0} - txClaim.AddTxIn(wire.NewTxIn(&settleOutpoint, nil, nil)) + //=========================================== + // Claim TX + //=========================================== - addr, err := wal.NewAdr() - txClaim.AddTxOut(wire.NewTxOut(d.ValueOurs-1000, lnutil.DirectWPKHScriptFromPKH(addr))) // todo calc fee - fee is double here because the contract output already had the fee deducted in the settlement TX - kg.Step[2] = UseContractPayoutBase - privSpend, _ := wal.GetPriv(kg) + // Here the transaction size is always the same + // n := 8 + VarIntSerializeSize(uint64(len(msg.TxIn))) + + // VarIntSerializeSize(uint64(len(msg.TxOut))) + // n = 10 + // Plus Single input 41 + // Plus Single output 31 + // Plus 2 for all wittness transactions + // Plus Witness Data 151 - pubSpend := wal.GetPub(kg) - privOracle, pubOracle := koblitz.PrivKeyFromBytes(koblitz.S256(), oracleSig[:]) - privContractOutput := lnutil.CombinePrivateKeys(privSpend, privOracle) + // TxSize = 4 + 4 + 1 + 1 + 2 + 151 + 41 + 31 = 235 + // Vsize = ((235 - 151 - 2) * 3 + 235) / 4 = 120,25 - var pubOracleBytes [33]byte - copy(pubOracleBytes[:], pubOracle.SerializeCompressed()) - var pubSpendBytes [33]byte - copy(pubSpendBytes[:], pubSpend.SerializeCompressed()) - settleScript := lnutil.DlcCommitScript(c.OurPayoutBase, pubOracleBytes, c.TheirPayoutBase, 5) - err = nd.SignClaimTx(txClaim, settleTx.TxOut[0].Value, settleScript, privContractOutput, false) - if err != nil { - logging.Errorf("SettleContract SignClaimTx err %s", err.Error()) - return [32]byte{}, [32]byte{}, err - } + if ( d.ValueOurs != 0){ - // Claim TX should be valid here, so publish it. - err = wal.DirectSendTx(txClaim) - if err != nil { - logging.Errorf("SettleContract DirectSendTx (claim) err %s", err.Error()) - return [32]byte{}, [32]byte{}, err - } + vsize := uint32(121) + fee := vsize * c.FeePerByte + + // TODO: Claim the contract settlement output back to our wallet - otherwise the peer can claim it after locktime. + txClaim := wire.NewMsgTx() + txClaim.Version = 2 + + settleOutpoint := wire.OutPoint{Hash: settleTx.TxHash(), Index: 0} + txClaim.AddTxIn(wire.NewTxIn(&settleOutpoint, nil, nil)) + + addr, err := wal.NewAdr() + txClaim.AddTxOut(wire.NewTxOut(settleTx.TxOut[0].Value-int64(fee), lnutil.DirectWPKHScriptFromPKH(addr))) + + kg.Step[2] = UseContractPayoutBase + privSpend, _ := wal.GetPriv(kg) + + pubSpend := wal.GetPub(kg) + privOracle, pubOracle := koblitz.PrivKeyFromBytes(koblitz.S256(), oracleSig[:]) + privContractOutput := lnutil.CombinePrivateKeys(privSpend, privOracle) + + var pubOracleBytes [33]byte + copy(pubOracleBytes[:], pubOracle.SerializeCompressed()) + var pubSpendBytes [33]byte + copy(pubSpendBytes[:], pubSpend.SerializeCompressed()) + + settleScript := lnutil.DlcCommitScript(c.OurPayoutBase, pubOracleBytes, c.TheirPayoutBase, 5) + err = nd.SignClaimTx(txClaim, settleTx.TxOut[0].Value, settleScript, privContractOutput, false) + if err != nil { + logging.Errorf("SettleContract SignClaimTx err %s", err.Error()) + return [32]byte{}, [32]byte{}, err + } + + // Claim TX should be valid here, so publish it. + err = wal.DirectSendTx(txClaim) + if err != nil { + logging.Errorf("SettleContract DirectSendTx (claim) err %s", err.Error()) + return [32]byte{}, [32]byte{}, err + } + + c.Status = lnutil.ContractStatusClosed + err = nd.DlcManager.SaveContract(c) + if err != nil { + return [32]byte{}, [32]byte{}, err + } + return settleTx.TxHash(), txClaim.TxHash(), nil + + }else{ + + return settleTx.TxHash(), [32]byte{}, nil - c.Status = lnutil.ContractStatusClosed - err = nd.DlcManager.SaveContract(c) - if err != nil { - return [32]byte{}, [32]byte{}, err } - return settleTx.TxHash(), txClaim.TxHash(), nil + } diff --git a/qln/msghandler.go b/qln/msghandler.go index d1e980c50..88b1aa35f 100644 --- a/qln/msghandler.go +++ b/qln/msghandler.go @@ -574,8 +574,24 @@ func (nd *LitNode) HandleContractOPEvent(c *lnutil.DlcContract, if err != nil { return err } - txClaim.AddTxOut(wire.NewTxOut(value-500, - lnutil.DirectWPKHScriptFromPKH(addr))) // todo calc fee + + // Here the transaction size is always the same + // n := 8 + VarIntSerializeSize(uint64(len(msg.TxIn))) + + // VarIntSerializeSize(uint64(len(msg.TxOut))) + // n = 10 + // Plus Single input 41 + // Plus Single output 31 + // Plus 2 for all wittness transactions + // Plus Witness Data 108 + + // TxSize = 4 + 4 + 1 + 1 + 2 + 108 + 41 + 31 = 192 + // Vsize = ((192 - 108 - 2) * 3 + 192) / 4 = 109,5 + + vsize := uint32(110) + fee := vsize * c.FeePerByte + + txClaim.AddTxOut(wire.NewTxOut(value-int64(fee), + lnutil.DirectWPKHScriptFromPKH(addr))) var kg portxo.KeyGen kg.Depth = 5