diff --git a/.gitignore b/.gitignore index 902c32b19..c4a5e8d9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Lit stuff lit +dlcoracle lit-af !cmd/lit-af cmd/lit-af/lit-af diff --git a/cmd/lit-af/dlccmds.go b/cmd/lit-af/dlccmds.go index 943806efd..c055575f1 100644 --- a/cmd/lit-af/dlccmds.go +++ b/cmd/lit-af/dlccmds.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" "time" + "errors" "github.com/fatih/color" "github.com/mit-dci/lit/litrpc" @@ -102,6 +103,9 @@ var contractCommand = &Command{ fmt.Sprintf("%-20s %s", lnutil.White("settime"), "Sets the settlement time of a contract"), + fmt.Sprintf("%-20s %s", + lnutil.White("setrefundtime"), + "Sets the refund time of a contract"), fmt.Sprintf("%-20s %s", lnutil.White("setdatafeed"), "Sets the data feed to use, will fetch the R point"), @@ -117,6 +121,11 @@ var contractCommand = &Command{ fmt.Sprintf("%-20s %s", lnutil.White("setcointype"), "Sets the cointype of a contract"), + fmt.Sprintf("%-20s %s", + lnutil.White("setfeeperbyte"), + fmt.Sprintf("%-20s %s", + lnutil.White("setoraclesnumber"), + "Sets the oracles number for a contract"), fmt.Sprintf("%-20s %s", lnutil.White("offer"), "Offer a draft contract to one of your peers"), @@ -238,6 +247,23 @@ var setContractSettlementTimeCommand = &Command{ ), ShortDescription: "Sets the settlement time for the contract\n", } + + +var setContractRefundTimeCommand = &Command{ + Format: fmt.Sprintf("%s%s\n", lnutil.White("dlc contract settime"), + lnutil.ReqColor("cid", "time")), + Description: fmt.Sprintf("%s\n%s\n%s\n", + "Sets the refund time for the contract", + fmt.Sprintf("%-10s %s", + lnutil.White("cid"), + "The ID of the contract"), + fmt.Sprintf("%-10s %s", + lnutil.White("time"), + "The refund time (unix timestamp)"), + ), + ShortDescription: "Sets the settlement time for the contract\n", +} + var setContractFundingCommand = &Command{ Format: fmt.Sprintf("%s%s\n", lnutil.White("dlc contract setfunding"), lnutil.ReqColor("cid", "ourAmount", "theirAmount")), @@ -289,6 +315,36 @@ var setContractCoinTypeCommand = &Command{ ), ShortDescription: "Sets the coin type to use for the contract\n", } +var setContractFeePerByteCommand = &Command{ + Format: fmt.Sprintf("%s%s\n", lnutil.White("dlc contract setfeeperbyte"), + lnutil.ReqColor("cid", "feeperbyte")), + Description: fmt.Sprintf("%s\n%s\n%s\n", + "Sets the fee per byte to use for the contract", + fmt.Sprintf("%-10s %s", + lnutil.White("cid"), + "The ID of the contract"), + fmt.Sprintf("%-10s %s", + lnutil.White("feeperbyte"), + "The fee per byte in satoshi to use for the contract"), + ), + ShortDescription: "Sets the fee per byte in satoshi to use for the contract\n", +} + +var setContractOraclesNumberCommand = &Command{ + Format: fmt.Sprintf("%s%s\n", lnutil.White("dlc contract setoraclesnumber"), + lnutil.ReqColor("cid", "oraclesnumber")), + Description: fmt.Sprintf("%s\n%s\n%s\n", + "Sets the oracles number to use for the contract", + fmt.Sprintf("%-10s %s", + lnutil.White("cid"), + "The ID of the contract"), + fmt.Sprintf("%-10s %s", + lnutil.White("oraclesnumber"), + "The oracles number to use for the contract"), + ), + ShortDescription: "Sets a number of oracles required for the contract\n", +} + var declineContractCommand = &Command{ Format: fmt.Sprintf("%s%s\n", lnutil.White("dlc contract decline"), lnutil.ReqColor("cid")), @@ -492,6 +548,10 @@ func (lc *litAfClient) DlcContract(textArgs []string) error { return lc.DlcSetContractSettlementTime(textArgs) } + if cmd == "setrefundtime" { + return lc.DlcSetContractRefundTime(textArgs) + } + if cmd == "setfunding" { return lc.DlcSetContractFunding(textArgs) } @@ -504,6 +564,14 @@ func (lc *litAfClient) DlcContract(textArgs []string) error { return lc.DlcSetContractCoinType(textArgs) } + if cmd == "setfeeperbyte" { + return lc.DlcSetContractFeePerByte(textArgs) + } + + if cmd == "setoraclesnumber" { + return lc.DlcSetContractOraclesNumber(textArgs) + } + if cmd == "offer" { return lc.DlcOfferContract(textArgs) } @@ -737,6 +805,38 @@ func (lc *litAfClient) DlcSetContractSettlementTime(textArgs []string) error { return nil } +func (lc *litAfClient) DlcSetContractRefundTime(textArgs []string) error { + stopEx, err := CheckHelpCommand(setContractRefundTimeCommand, textArgs, 2) + if err != nil || stopEx { + return err + } + + args := new(litrpc.SetContractSettlementTimeArgs) + reply := new(litrpc.SetContractSettlementTimeReply) + + cIdx, err := strconv.ParseUint(textArgs[0], 10, 64) + if err != nil { + return err + } + time, err := strconv.ParseUint(textArgs[1], 10, 64) + if err != nil { + return err + } + args.CIdx = cIdx + args.Time = time + + err = lc.Call("LitRPC.SetContractRefundTime", args, reply) + if err != nil { + return err + } + + fmt.Fprint(color.Output, "Refund time set successfully\n") + + return nil +} + + + func (lc *litAfClient) DlcSetContractFunding(textArgs []string) error { stopEx, err := CheckHelpCommand(setContractFundingCommand, textArgs, 3) if err != nil || stopEx { @@ -803,6 +903,79 @@ func (lc *litAfClient) DlcSetContractCoinType(textArgs []string) error { return nil } + +func (lc *litAfClient) DlcSetContractFeePerByte(textArgs []string) error { + stopEx, err := CheckHelpCommand(setContractFeePerByteCommand, textArgs, 2) + if err != nil || stopEx { + return err + } + + args := new(litrpc.SetContractFeePerByteArgs) + reply := new(litrpc.SetContractFeePerByteReply) + + cIdx, err := strconv.ParseUint(textArgs[0], 10, 64) + if err != nil { + return err + } + feeperbyte, err := strconv.ParseUint(textArgs[1], 10, 64) + if err != nil { + return err + } + + args.CIdx = cIdx + args.FeePerByte = uint32(feeperbyte) + + err = lc.Call("LitRPC.SetContractFeePerByte", args, reply) + if err != nil { + return err + } + + fmt.Fprint(color.Output, "Fee per byte set successfully\n") + + return nil +} + + + + +func (lc *litAfClient) DlcSetContractOraclesNumber(textArgs []string) error { + stopEx, err := CheckHelpCommand(setContractOraclesNumberCommand, textArgs, 2) + if err != nil || stopEx { + return err + } + + args := new(litrpc.SetContractOraclesNumberArgs) + reply := new(litrpc.SetContractOraclesNumberReply) + + cIdx, err := strconv.ParseUint(textArgs[0], 10, 64) + if err != nil { + return err + } + OraclesNumber, err := strconv.ParseUint(textArgs[1], 10, 64) + if err != nil { + return err + } + + if OraclesNumber > 1 { + return errors.New("Multiple oracles supported only from RPC cals.") + } + + args.CIdx = cIdx + args.OraclesNumber = uint32(OraclesNumber) + + err = lc.Call("LitRPC.SetContractOraclesNumber", args, reply) + if err != nil { + return err + } + + fmt.Fprint(color.Output, "SetContractOraclesNumber set successfully\n") + + return nil +} + + + + func (lc *litAfClient) DlcSetContractDivision(textArgs []string) error { stopEx, err := CheckHelpCommand(setContractDivisionCommand, textArgs, 3) if err != nil || stopEx { @@ -956,19 +1129,27 @@ func PrintContract(c *lnutil.DlcContract) { fmt.Fprintf(color.Output, "%-30s : %d\n", lnutil.White("Index"), c.Idx) fmt.Fprintf(color.Output, "%-30s : [%x...%x...%x]\n", lnutil.White("Oracle public key"), - c.OracleA[:2], c.OracleA[15:16], c.OracleA[31:]) + c.OracleA[0][:2], c.OracleA[0][15:16], c.OracleA[0][31:]) fmt.Fprintf(color.Output, "%-30s : [%x...%x...%x]\n", lnutil.White("Oracle R-point"), c.OracleR[:2], - c.OracleR[15:16], c.OracleR[31:]) + c.OracleR[0][15:16], c.OracleR[0][31:]) fmt.Fprintf(color.Output, "%-30s : %s\n", lnutil.White("Settlement time"), time.Unix(int64(c.OracleTimestamp), 0).UTC().Format(time.UnixDate)) + fmt.Fprintf(color.Output, "%-30s : %s\n", + lnutil.White("Refund time"), + time.Unix(int64(c.RefundTimestamp), 0).UTC().Format(time.UnixDate)) fmt.Fprintf(color.Output, "%-30s : %d\n", lnutil.White("Funded by us"), c.OurFundingAmount) fmt.Fprintf(color.Output, "%-30s : %d\n", lnutil.White("Funded by peer"), c.TheirFundingAmount) fmt.Fprintf(color.Output, "%-30s : %d\n", lnutil.White("Coin type"), c.CoinType) + fmt.Fprintf(color.Output, "%-30s : %d\n", + lnutil.White("Fee per byte"), c.FeePerByte) + fmt.Fprintf(color.Output, "%-30s : %d\n", + lnutil.White("Oracles number"), c.OraclesNumber) + peer := "None" if c.PeerIdx > 0 { diff --git a/consts/consts.go b/consts/consts.go index b4dbe7d4e..9d076e2e0 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -24,4 +24,5 @@ const ( QcStateFee = 10 // fixqcstatefee DefaultLockTime = 500 //default lock time DlcSettlementTxFee = 1000 + MaxOraclesNumber = 8 ) diff --git a/dlc/contract.go b/dlc/contract.go index cc786cbdf..3ba479e66 100644 --- a/dlc/contract.go +++ b/dlc/contract.go @@ -4,9 +4,12 @@ import ( "fmt" "github.com/mit-dci/lit/lnutil" + "github.com/mit-dci/lit/consts" ) const COINTYPE_NOT_SET = ^uint32(0) // Max Uint +const FEEPERBYTE_NOT_SET = ^uint32(0) // Max Uint +const ORACLESNUMBER_NOT_SET = ^uint32(0) // Max Uint // AddContract starts a new draft contract func (mgr *DlcManager) AddContract() (*lnutil.DlcContract, error) { @@ -15,6 +18,8 @@ func (mgr *DlcManager) AddContract() (*lnutil.DlcContract, error) { c := new(lnutil.DlcContract) c.Status = lnutil.ContractStatusDraft c.CoinType = COINTYPE_NOT_SET + c.FeePerByte = FEEPERBYTE_NOT_SET + c.OraclesNumber = ORACLESNUMBER_NOT_SET err = mgr.SaveContract(c) if err != nil { return nil, err @@ -26,30 +31,38 @@ func (mgr *DlcManager) AddContract() (*lnutil.DlcContract, error) { // SetContractOracle assigns a particular oracle to a contract - used for // determining which pubkey A to use and can also allow for fetching R-points // automatically when the oracle was imported from a REST api -func (mgr *DlcManager) SetContractOracle(cIdx, oIdx uint64) error { +func (mgr *DlcManager) SetContractOracle(cIdx uint64, oIdx []uint64) 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 oracle unless the" + " contract is in Draft state") } - o, err := mgr.LoadOracle(oIdx) - if err != nil { - return err - } - c.OracleA = o.A + if c.OraclesNumber == ORACLESNUMBER_NOT_SET { + return fmt.Errorf("You need to set the OraclesNumber variable.") + } + + if len(oIdx) < int(c.OraclesNumber) { + return fmt.Errorf("You cannot set the number of oracles in less than" + + " in a variable OraclesNumber") + } - // Reset the R point when changing the oracle - c.OracleR = [33]byte{} + for i := uint64(1); i <= uint64(c.OraclesNumber); i++ { + o, err := mgr.LoadOracle(i) + if err != nil { + return err + } + c.OracleA[i-1] = o.A + // Reset R point after oracle setting + c.OracleR[i-1] = [33]byte{} + } mgr.SaveContract(c) - return nil } @@ -60,25 +73,46 @@ func (mgr *DlcManager) SetContractSettlementTime(cIdx, time uint64) error { if err != nil { return err } - if c.Status != lnutil.ContractStatusDraft { return fmt.Errorf("You cannot change or set the settlement time" + " unless the contract is in Draft state") } - c.OracleTimestamp = time + fmt.Printf("c.OracleTimestamp %d \n", c.OracleTimestamp) + // Reset the R point - c.OracleR = [33]byte{} + for i, _ := range c.OracleR{ + c.OracleR[i] = [33]byte{} + } mgr.SaveContract(c) + return nil +} + +// SetContractRefundTime. If until this time Oracle does not publish the data, +// then either party can publish a RefundTransaction +func (mgr *DlcManager) SetContractRefundTime(cIdx, time uint64) 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 settlement time" + + " unless the contract is in Draft state") + } + c.RefundTimestamp = time + mgr.SaveContract(c) return nil } + + // SetContractDatafeed will automatically fetch the R-point from the REST API, // if an oracle is imported from a REST API. You need to set the settlement time // first, because the R point is a key unique for the time and feed +// TODO change for multiple oracles. func (mgr *DlcManager) SetContractDatafeed(cIdx, feed uint64) error { c, err := mgr.LoadContract(cIdx) if err != nil { @@ -95,12 +129,12 @@ func (mgr *DlcManager) SetContractDatafeed(cIdx, feed uint64) error { " otherwise no R point can be retrieved for the feed") } - o, err := mgr.FindOracleByKey(c.OracleA) + o, err := mgr.FindOracleByKey(c.OracleA[0]) if err != nil { return err } - c.OracleR, err = o.FetchRPoint(feed, c.OracleTimestamp) + c.OracleR[0], err = o.FetchRPoint(feed, c.OracleTimestamp) if err != nil { return err } @@ -114,7 +148,7 @@ func (mgr *DlcManager) SetContractDatafeed(cIdx, feed uint64) error { // SetContractRPoint allows you to manually set the R-point key if an oracle is // not imported from a REST API -func (mgr *DlcManager) SetContractRPoint(cIdx uint64, rPoint [33]byte) error { +func (mgr *DlcManager) SetContractRPoint(cIdx uint64, rPoint [][33]byte) error { c, err := mgr.LoadContract(cIdx) if err != nil { return err @@ -125,7 +159,11 @@ func (mgr *DlcManager) SetContractRPoint(cIdx uint64, rPoint [33]byte) error { " contract is in Draft state") } - c.OracleR = rPoint + for i:=uint32(0); i < c.OraclesNumber; i++{ + c.OracleR[i] = rPoint[i] + } + + err = mgr.SaveContract(c) if err != nil { @@ -244,3 +282,50 @@ 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 fee per byte unless" + + " the contract is in Draft state") + } + + c.FeePerByte = feeperbyte + + mgr.SaveContract(c) + + return nil +} + + + +//SetContractOraclesNumber sets a number of oracles required for the contract. +func (mgr *DlcManager) SetContractOraclesNumber(cIdx uint64, oraclesNumber 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 OraclesNumber unless" + + " the contract is in Draft state") + } + + + if oraclesNumber > consts.MaxOraclesNumber{ + return fmt.Errorf("You cannot set OraclesNumber greater that %d (consts.MaxOraclesNumber)", consts.MaxOraclesNumber) + } + + c.OraclesNumber = oraclesNumber + + mgr.SaveContract(c) + + return nil +} diff --git a/docs/execute-dlc-litaf.md b/docs/execute-dlc-litaf.md index 6a4cc53cc..9ef9e02c1 100644 --- a/docs/execute-dlc-litaf.md +++ b/docs/execute-dlc-litaf.md @@ -52,6 +52,13 @@ First, we create a new draft contract dlc contract new ``` +Multiple oracles supported only from RPC cals. +With lit-af utility you have to set oracles number to 1. + +``` +dlc contract setoraclesnumber 1 1 +``` + Then, we configure the contract to use the oracle we added in step 2. Remember, if this oracle got a different ID, you have to adjust the second parameter to this call. ``` @@ -64,6 +71,13 @@ Next, we configure the timestamp at which the contract will settle. This is a ti dlc contract settime 1 1528848000 ``` +Now we have to configure refund timestamp after which the refund transaction becomes valid. + +``` +dlc contract setrefundtime 1 1528848000 +``` +In this case the refund transaction becomes valid at the same time. + Then, we configure the R-point for the contract, as mentioned earlier this is the public key to the one-time signing key used by the oracle to sign the value it will publish. ``` diff --git a/litrpc/dlccmds.go b/litrpc/dlccmds.go index bb6d84cb2..33b21f461 100644 --- a/litrpc/dlccmds.go +++ b/litrpc/dlccmds.go @@ -2,9 +2,11 @@ package litrpc import ( "encoding/hex" + "fmt" "github.com/mit-dci/lit/dlc" "github.com/mit-dci/lit/lnutil" + "github.com/mit-dci/lit/consts" ) type ListOraclesArgs struct { @@ -142,7 +144,7 @@ func (r *LitRPC) GetContract(args GetContractArgs, type SetContractOracleArgs struct { CIdx uint64 - OIdx uint64 + OIdx []uint64 } type SetContractOracleReply struct { @@ -189,7 +191,7 @@ func (r *LitRPC) SetContractDatafeed(args SetContractDatafeedArgs, type SetContractRPointArgs struct { CIdx uint64 - RPoint [33]byte + RPoint [][33]byte } type SetContractRPointReply struct { @@ -219,8 +221,8 @@ type SetContractSettlementTimeReply struct { Success bool } -// SetContractSettlementTime sets the time this contract will settle (the -// unix epoch) +// SetContractSettlementTime sets the time this the oracle will publish data ( +// unix time) func (r *LitRPC) SetContractSettlementTime(args SetContractSettlementTimeArgs, reply *SetContractSettlementTimeReply) error { var err error @@ -234,6 +236,24 @@ func (r *LitRPC) SetContractSettlementTime(args SetContractSettlementTimeArgs, return nil } + +// SetContractRefundTime. If until this time Oracle does not publish the data, +// then either party can publish a RefundTransaction +func (r *LitRPC) SetContractRefundTime(args SetContractSettlementTimeArgs, + reply *SetContractSettlementTimeReply) error { + var err error + + err = r.Node.DlcManager.SetContractRefundTime(args.CIdx, args.Time) + if err != nil { + return err + } + + reply.Success = true + return nil +} + + + type SetContractFundingArgs struct { CIdx uint64 OurAmount int64 @@ -314,6 +334,93 @@ func (r *LitRPC) SetContractCoinType(args SetContractCoinTypeArgs, return nil } + +type SetContractFeePerByteArgs struct { + CIdx uint64 + FeePerByte uint32 +} + +type SetContractFeePerByteReply struct { + Success bool +} + + +// SetContractFeePerByte sets the fee per byte for 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 SetContractOraclesNumberArgs struct { + CIdx uint64 + OraclesNumber uint32 +} + +type SetContractOraclesNumberReply struct { + Success bool +} + + +func (r *LitRPC) SetContractOraclesNumber(args SetContractOraclesNumberArgs, + reply *SetContractOraclesNumberReply) error { + var err error + + err = r.Node.DlcManager.SetContractOraclesNumber(args.CIdx, args.OraclesNumber) + if err != nil { + return err + } + + reply.Success = true + return nil +} + +type GetContractDivisionArgs struct { + CIdx uint64 + OracleValue int64 +} + +type GetContractDivisionReply struct { + ValueOurs int64 +} + +// GetContractDivision +func (r *LitRPC) GetContractDivision(args GetContractDivisionArgs, + reply *GetContractDivisionReply) error { + + //err = r.Node.DlcManager.GetContractDivision(args.CIdx, args.OracleValue) + + c, err1 := r.Node.DlcManager.LoadContract(args.CIdx) + if err1 != nil { + fmt.Errorf("GetContractDivision(): LoadContract err %s\n", err1.Error()) + return err1 + } + + + d, err2 := c.GetDivision(args.OracleValue) + if err2 != nil { + fmt.Errorf("GetContractDivision(): c.GetDivision err %s\n", err2.Error()) + return err2 + } + reply.ValueOurs = d.ValueOurs + + return nil +} + + +//----------------------------------------------------------- + + + type OfferContractArgs struct { CIdx uint64 PeerIdx uint32 @@ -368,7 +475,7 @@ func (r *LitRPC) ContractRespond(args ContractRespondArgs, reply *ContractRespon type SettleContractArgs struct { CIdx uint64 OracleValue int64 - OracleSig [32]byte + OracleSig [consts.MaxOraclesNumber][32]byte } type SettleContractReply struct { @@ -393,3 +500,29 @@ func (r *LitRPC) SettleContract(args SettleContractArgs, reply.Success = true return nil } + + +//====================================================================== + + +type RefundContractArgs struct { + CIdx uint64 +} + +type RefundContractReply struct { + Success bool +} + +// RefundContract + +func (r *LitRPC) RefundContract(args RefundContractArgs,reply *RefundContractReply) error { + var err error + + reply.Success, err = r.Node.RefundContract(args.CIdx) + if err != nil { + return err + } + + reply.Success = true + return nil +} diff --git a/litrpc/lndcrpcclient.go b/litrpc/lndcrpcclient.go index 7a508ee0b..b5372615d 100644 --- a/litrpc/lndcrpcclient.go +++ b/litrpc/lndcrpcclient.go @@ -28,9 +28,11 @@ type LndcRpcClient struct { requestNonce uint64 requestNonceMtx sync.Mutex responseChannelMtx sync.Mutex - responseChannels map[uint64]chan lnutil.RemoteControlRpcResponseMsg + responseChannels map[uint64]chan *lnutil.RemoteControlRpcResponseMsg key *koblitz.PrivateKey conMtx sync.Mutex + + chunksOfMsg map[int64]*lnutil.ChunkMsg } // LndcRpcCanConnectLocally checks if we can connect to lit using the normal @@ -116,9 +118,12 @@ func NewLndcRpcClient(address string, key *koblitz.PrivateKey) (*LndcRpcClient, var err error cli := new(LndcRpcClient) + + cli.chunksOfMsg = make(map[int64]*lnutil.ChunkMsg) + // Create a map of chan objects to receive returned responses on. These channels // are sent to from the ReceiveLoop, and awaited in the Call method. - cli.responseChannels = make(map[uint64]chan lnutil.RemoteControlRpcResponseMsg) + cli.responseChannels = make(map[uint64]chan *lnutil.RemoteControlRpcResponseMsg) //Parse the address we're connecting to who, where := lnutil.ParseAdrString(address) @@ -158,7 +163,7 @@ func (cli *LndcRpcClient) Call(serviceMethod string, args interface{}, reply int // Create the channel to receive the reply on cli.responseChannelMtx.Lock() - cli.responseChannels[nonce] = make(chan lnutil.RemoteControlRpcResponseMsg) + cli.responseChannels[nonce] = make(chan *lnutil.RemoteControlRpcResponseMsg) cli.responseChannelMtx.Unlock() // Send the message in a goroutine @@ -221,6 +226,35 @@ func (cli *LndcRpcClient) ReceiveLoop() { return } msg = msg[:n] + + if msg[0] == lnutil.MSGID_CHUNKS_BEGIN { + + beginChunksMsg, _ := lnutil.NewChunksBeginMsgFromBytes(msg, 0) + + msg_tmp := new(lnutil.ChunkMsg) + msg_tmp.TimeStamp = beginChunksMsg.TimeStamp + cli.chunksOfMsg[beginChunksMsg.TimeStamp] = msg_tmp + + continue + } + + if msg[0] == lnutil.MSGID_CHUNK_BODY { + + chunkMsg, _ := lnutil.NewChunkMsgFromBytes(msg, 0) + cli.chunksOfMsg[chunkMsg.TimeStamp].Data = append(cli.chunksOfMsg[chunkMsg.TimeStamp].Data, chunkMsg.Data...) + + continue + } + + if msg[0] == lnutil.MSGID_CHUNKS_END { + + endChunksMsg, _ := lnutil.NewChunksBeginMsgFromBytes(msg, 0) + msg = cli.chunksOfMsg[endChunksMsg.TimeStamp].Data + + } + + + // We only care about RPC responses (for now) if msg[0] == lnutil.MSGID_REMOTE_RPCRESPONSE { // Parse the received message @@ -239,7 +273,7 @@ func (cli *LndcRpcClient) ReceiveLoop() { // reply and therefore, it could have not blocked and just // ignore the return value. select { - case responseChan <- response: + case responseChan <- &response: default: } diff --git a/lndc/conn.go b/lndc/conn.go index 01c1f189c..afc786162 100644 --- a/lndc/conn.go +++ b/lndc/conn.go @@ -7,6 +7,7 @@ import ( "math" "net" "time" + "encoding/binary" "github.com/mit-dci/lit/crypto/koblitz" "github.com/mit-dci/lit/lnutil" @@ -151,27 +152,62 @@ func (c *Conn) Write(b []byte) (n int, err error) { // If we need to split the message into fragments, then we'll write // chunks which maximize usage of the available payload. - chunkSize := math.MaxUint16 + chunkSize := math.MaxUint16 - 1000 + + lenb := int(len(b)) + curts := (time.Now().UnixNano()) + + beginChunksMsgBodyBytes := new(bytes.Buffer) + beginChunksMsgBodyBytes.WriteByte(lnutil.MSGID_CHUNKS_BEGIN) + binary.Write(beginChunksMsgBodyBytes, binary.BigEndian, curts) + + // Start saving chunks + chunk_err := c.noise.WriteMessage(c.conn, beginChunksMsgBodyBytes.Bytes()) + if chunk_err != nil { + return 0, chunk_err + } - bytesToWrite := len(b) bytesWritten := 0 + bytesToWrite := lenb + for bytesWritten < bytesToWrite { // If we're on the last chunk, then truncate the chunk size as // necessary to avoid an out-of-bounds array memory access. if bytesWritten+chunkSize > len(b) { - chunkSize = len(b) - bytesWritten + chunkSize = lenb - bytesWritten } // Slice off the next chunk to be written based on our running // counter and next chunk size. chunk := b[bytesWritten : bytesWritten+chunkSize] - if err := c.noise.WriteMessage(c.conn, chunk); err != nil { - return bytesWritten, err + + // Wrap chunk in a MSGID_CHUNK_BODY message + chunkMsgBodyBytes := new(bytes.Buffer) + chunkMsgBodyBytes.WriteByte(lnutil.MSGID_CHUNK_BODY) + binary.Write(chunkMsgBodyBytes, binary.BigEndian, curts) + binary.Write(chunkMsgBodyBytes, binary.BigEndian, int32(chunkSize)) + chunkMsgBodyBytes.Write(chunk) + + + chunk_err = c.noise.WriteMessage(c.conn, chunkMsgBodyBytes.Bytes()) + if chunk_err != nil { + return 0, chunk_err } bytesWritten += len(chunk) } + // Actually send a message (unwrap and send) + endChunksMsgBodyBytes := new(bytes.Buffer) + endChunksMsgBodyBytes.WriteByte(lnutil.MSGID_CHUNKS_END) + binary.Write(endChunksMsgBodyBytes, binary.BigEndian, curts) + + chunk_err = c.noise.WriteMessage(c.conn, endChunksMsgBodyBytes.Bytes()) + if chunk_err != nil { + return 0, chunk_err + } + + return bytesWritten, nil } diff --git a/lnp2p/msgproc.go b/lnp2p/msgproc.go index 3897a9580..3df578fe8 100644 --- a/lnp2p/msgproc.go +++ b/lnp2p/msgproc.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/mit-dci/lit/logging" "sync" + "github.com/mit-dci/lit/lnutil" ) // ParseFuncType is the type of a Message parser function. @@ -28,6 +29,8 @@ type MessageProcessor struct { // TODO Evaluate if this mutex is even necessary? active bool actmtx *sync.Mutex + + ChunksOfMsg map[int64]*lnutil.ChunkMsg } // NewMessageProcessor processes messages coming in from over the network. @@ -36,6 +39,7 @@ func NewMessageProcessor() MessageProcessor { handlers: [256]*messagehandler{}, active: false, actmtx: &sync.Mutex{}, + ChunksOfMsg: make(map[int64]*lnutil.ChunkMsg), } } @@ -77,6 +81,43 @@ func (mp *MessageProcessor) HandleMessage(peer *Peer, buf []byte) error { // First see if we have handlers defined for this message type. mtype := buf[0] + + if mtype == 0xB2{ + + msg, _ := lnutil.NewChunksBeginMsgFromBytes(buf, peer.GetIdx()) + + chunk_msg := new(lnutil.ChunkMsg) + chunk_msg.TimeStamp = msg.TimeStamp + + mp.ChunksOfMsg[msg.TimeStamp] = chunk_msg + + return nil + + } + + if mtype == 0xB3{ + + msg, _ := lnutil.NewChunkMsgFromBytes(buf, peer.GetIdx()) + mp.ChunksOfMsg[msg.TimeStamp].Data = append(mp.ChunksOfMsg[msg.TimeStamp].Data, msg.Data...) + + return nil + + } + + if mtype == 0xB4{ + + msg, _ := lnutil.NewChunksEndMsgFromBytes(buf, peer.GetIdx()) + + buf = mp.ChunksOfMsg[msg.TimeStamp].Data + mtype = buf[0] + + delete(mp.ChunksOfMsg, msg.TimeStamp) + + } + + + + h := mp.handlers[mtype] if h == nil { return fmt.Errorf("no handler found for messasge of type %x", mtype) diff --git a/lnp2p/peermgr.go b/lnp2p/peermgr.go index f367ddd83..068d44fd6 100644 --- a/lnp2p/peermgr.go +++ b/lnp2p/peermgr.go @@ -7,6 +7,7 @@ import ( "net" "sync" "time" + "math" "github.com/mit-dci/lit/btcutil/hdkeychain" "github.com/mit-dci/lit/crypto/koblitz" @@ -87,6 +88,25 @@ func NewPeerManager(rootkey *hdkeychain.ExtendedKey, pdb lncore.LitPeerStorage, mtx: &sync.Mutex{}, } + + // Clear ChunksOfMsg in case of incomplete chunks transmittion. + // Try to clean the map every 5 minutes. Therefore message have to + // be transmitted within a 5 minutes. + go func(){ + + for { + + time.Sleep(5 * time.Minute) + + for k := range pm.mproc.ChunksOfMsg { + tdelta := time.Now().UnixNano() - k + if tdelta > 3*int64(math.Pow10(11)) { + delete(pm.mproc.ChunksOfMsg, k) + } + } + } + }() + return pm, nil } diff --git a/lnutil/dlclib.go b/lnutil/dlclib.go index a45c6724a..843388fbd 100644 --- a/lnutil/dlclib.go +++ b/lnutil/dlclib.go @@ -6,12 +6,18 @@ 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" + + "github.com/mit-dci/lit/btcutil/txscript" + "github.com/mit-dci/lit/btcutil/txsort" + "github.com/mit-dci/lit/sig64" + "github.com/mit-dci/lit/consts" + ) // DlcContractStatus is an enumeration containing the various statuses a @@ -48,10 +54,16 @@ type DlcContract struct { PeerIdx uint32 // Coin type CoinType uint32 + // Fee per byte + FeePerByte uint32 + // It is a number of oracles required for the contract. + OraclesNumber uint32 // Pub keys of the oracle and the R point used in the contract - OracleA, OracleR [33]byte + OracleA, OracleR [consts.MaxOraclesNumber][33]byte // The time we expect the oracle to publish OracleTimestamp uint64 + // The time after which the refund transaction becomes valid. + RefundTimestamp uint64 // The payout specification Division []DlcContractDivision // The amounts either side are funding @@ -60,6 +72,9 @@ type DlcContract struct { OurChangePKH, TheirChangePKH [20]byte // Pubkey used in the funding multisig output OurFundMultisigPub, TheirFundMultisigPub [33]byte + //OurRevokePub, TheirRevokePub [33]byte + OurRefundPKH, TheirRefundPKH [20]byte + OurrefundTxSig64, TheirrefundTxSig64 [64]byte // Pubkey to be used in the commit script (combined with oracle pubkey // or CSV timeout) OurPayoutBase, TheirPayoutBase [33]byte @@ -109,9 +124,6 @@ func DlcContractFromBytes(b []byte) (*DlcContract, error) { } c.TheirIdx = theirIdx - copy(c.OracleA[:], buf.Next(33)) - copy(c.OracleR[:], buf.Next(33)) - peerIdx, err := wire.ReadVarInt(buf, 0) if err != nil { logging.Errorf("Error while deserializing varint for peerIdx: %s", err.Error()) @@ -125,10 +137,37 @@ 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 feePerByte: %s", err.Error()) + return nil, err + } + c.FeePerByte = uint32(feePerByte) + + + oraclesNumber, err := wire.ReadVarInt(buf, 0) + if err != nil { + logging.Errorf("Error while deserializing varint for oraclesNumber: %s", err.Error()) + return nil, err + } + + c.OraclesNumber = uint32(oraclesNumber) + + for i := uint64(0); i < uint64(consts.MaxOraclesNumber); i++ { + + copy(c.OracleA[i][:], buf.Next(33)) + copy(c.OracleR[i][:], buf.Next(33)) + } + c.OracleTimestamp, err = wire.ReadVarInt(buf, 0) if err != nil { return nil, err } + c.RefundTimestamp, err = wire.ReadVarInt(buf, 0) + if err != nil { + return nil, err + } ourFundingAmount, err := wire.ReadVarInt(buf, 0) if err != nil { return nil, err @@ -146,6 +185,12 @@ func DlcContractFromBytes(b []byte) (*DlcContract, error) { copy(c.OurFundMultisigPub[:], buf.Next(33)) copy(c.TheirFundMultisigPub[:], buf.Next(33)) + copy(c.OurRefundPKH[:], buf.Next(20)) + copy(c.TheirRefundPKH[:], buf.Next(20)) + + copy(c.OurrefundTxSig64[:], buf.Next(64)) + copy(c.TheirrefundTxSig64[:], buf.Next(64)) + copy(c.OurPayoutBase[:], buf.Next(33)) copy(c.TheirPayoutBase[:], buf.Next(33)) @@ -241,11 +286,25 @@ func (self *DlcContract) Bytes() []byte { wire.WriteVarInt(&buf, 0, uint64(self.Idx)) wire.WriteVarInt(&buf, 0, uint64(self.TheirIdx)) - buf.Write(self.OracleA[:]) - 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.OraclesNumber)) + + //fmt.Printf("Bytes() self.OraclesNumber: %d\n", self.OraclesNumber) + + + for i := uint64(0); i < uint64(consts.MaxOraclesNumber); i++ { + + //fmt.Printf("Bytes() i: %d\n", i) + + buf.Write(self.OracleA[i][:]) + buf.Write(self.OracleR[i][:]) + } + wire.WriteVarInt(&buf, 0, uint64(self.OracleTimestamp)) + wire.WriteVarInt(&buf, 0, uint64(self.RefundTimestamp)) wire.WriteVarInt(&buf, 0, uint64(self.OurFundingAmount)) wire.WriteVarInt(&buf, 0, uint64(self.TheirFundingAmount)) @@ -253,6 +312,13 @@ func (self *DlcContract) Bytes() []byte { buf.Write(self.TheirChangePKH[:]) buf.Write(self.OurFundMultisigPub[:]) buf.Write(self.TheirFundMultisigPub[:]) + + buf.Write(self.OurRefundPKH[:]) + buf.Write(self.TheirRefundPKH[:]) + + buf.Write(self.OurrefundTxSig64[:]) + buf.Write(self.TheirrefundTxSig64[:]) + buf.Write(self.OurPayoutBase[:]) buf.Write(self.TheirPayoutBase[:]) buf.Write(self.OurPayoutPKH[:]) @@ -338,8 +404,9 @@ func PrintTx(tx *wire.MsgTx) { // DlcOutput returns a Txo for a particular value that pays to // (PubKeyPeer+PubKeyOracleSig or (OurPubKey and TimeDelay)) -func DlcOutput(pkPeer, pkOracleSig, pkOurs [33]byte, value int64) *wire.TxOut { - scriptBytes := DlcCommitScript(pkPeer, pkOracleSig, pkOurs, 5) +func DlcOutput(pkPeer, pkOurs [33]byte, oraclesSigPub [][33]byte, value int64) *wire.TxOut { + + scriptBytes := DlcCommitScript(pkPeer, pkOurs, oraclesSigPub, 5) scriptBytes = P2WSHify(scriptBytes) return wire.NewTxOut(value, scriptBytes) @@ -351,10 +418,15 @@ func DlcOutput(pkPeer, pkOracleSig, pkOurs [33]byte, value int64) *wire.TxOut { // signature and their own private key to claim the funds from the output. // However, if they send the wrong one, they won't be able to claim the funds // and we can claim them once the time delay has passed. -func DlcCommitScript(pubKeyPeer, pubKeyOracleSig, ourPubKey [33]byte, - delay uint16) []byte { +func DlcCommitScript(pubKeyPeer, ourPubKey [33]byte, oraclesSigPub [][33]byte, delay uint16) []byte { // Combine pubKey and Oracle Sig - combinedPubKey := CombinePubs(pubKeyPeer, pubKeyOracleSig) + + combinedPubKey := CombinePubs(pubKeyPeer, oraclesSigPub[0]) + + for i := 1; i < len(oraclesSigPub); i++ { + combinedPubKey = CombinePubs(combinedPubKey, oraclesSigPub[i]) + } + return CommitScript(combinedPubKey, ourPubKey, delay) } @@ -441,71 +513,204 @@ 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(maxVsize) + // 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(150) + }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) // Also if we win less that the fees, our prize goes + // to a counterparty to increase his fee for a tx. + 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(150) + } + 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)) binary.Write(&buf, binary.BigEndian, uint64(0)) binary.Write(&buf, binary.BigEndian, d.OracleValue) - oracleSigPub, err := DlcCalcOracleSignaturePubKey(buf.Bytes(), - c.OracleA, c.OracleR) - if err != nil { - return nil, err + + + var oraclesSigPub [][33]byte + + for i:=uint32(0); i < c.OraclesNumber; i++ { + + res, err := DlcCalcOracleSignaturePubKey(buf.Bytes(),c.OracleA[i], c.OracleR[i]) + if err != nil { + return nil, err + } + + oraclesSigPub = append(oraclesSigPub, res) + } // Ours = the one we generate & sign. Theirs (ours = false) = the one they // generated, so we can use their sigs if ours { if valueTheirs > 0 { - tx.AddTxOut(DlcOutput(c.TheirPayoutBase, oracleSigPub, - c.OurPayoutBase, valueTheirs)) + tx.AddTxOut(DlcOutput(c.TheirPayoutBase, c.OurPayoutBase, oraclesSigPub, valueTheirs)) } if valueOurs > 0 { - tx.AddTxOut(wire.NewTxOut(valueOurs, - DirectWPKHScriptFromPKH(c.OurPayoutPKH))) + tx.AddTxOut(wire.NewTxOut(valueOurs, DirectWPKHScriptFromPKH(c.OurPayoutPKH))) } } else { if valueOurs > 0 { - tx.AddTxOut(DlcOutput(c.OurPayoutBase, oracleSigPub, - c.TheirPayoutBase, valueOurs)) + tx.AddTxOut(DlcOutput(c.OurPayoutBase, c.TheirPayoutBase, oraclesSigPub, valueOurs)) } if valueTheirs > 0 { - tx.AddTxOut(wire.NewTxOut(valueTheirs, - DirectWPKHScriptFromPKH(c.TheirPayoutPKH))) + tx.AddTxOut(wire.NewTxOut(valueTheirs, DirectWPKHScriptFromPKH(c.TheirPayoutPKH))) } } return tx, nil } + + + + +// RefundTx returns the transaction to refund the contract +// in the case the oracle does not publish a value. +func RefundTx(c *DlcContract) (*wire.MsgTx, error) { + + vsize := uint32(169) + fee := int64(vsize * c.FeePerByte) + + tx := wire.NewMsgTx() + tx.Version = 2 + tx.LockTime = uint32(c.RefundTimestamp) + + txin := wire.NewTxIn(&c.FundingOutpoint, nil, nil) + txin.Sequence = 0 + tx.AddTxIn(txin) + + ourRefScript := DirectWPKHScriptFromPKH(c.OurRefundPKH) + ourOutput := wire.NewTxOut(c.OurFundingAmount - fee, ourRefScript) + tx.AddTxOut(ourOutput) + + theirRefScript := DirectWPKHScriptFromPKH(c.TheirRefundPKH) + theirOutput := wire.NewTxOut(c.TheirFundingAmount - fee, theirRefScript) + tx.AddTxOut(theirOutput) + + txsort.InPlaceSort(tx) + + + return tx, nil + +} + + +// SignRefundTx +func SignRefundTx(c *DlcContract, tx *wire.MsgTx, priv *koblitz.PrivateKey) (error) { + + pre, _, err := FundTxScript(c.OurFundMultisigPub, c.TheirFundMultisigPub) + if err != nil { + return err + } + + hCache := txscript.NewTxSigHashes(tx) + + sig, err := txscript.RawTxInWitnessSignature(tx, hCache, 0, c.OurFundingAmount+c.TheirFundingAmount, pre, txscript.SigHashAll, priv) + if err != nil { + return err + } + + sig = sig[:len(sig)-1] + sig64 , _ := sig64.SigCompress(sig) + c.OurrefundTxSig64 = sig64 + + return nil + +} \ No newline at end of file diff --git a/lnutil/msglib.go b/lnutil/msglib.go index ae4599aa2..987cade6f 100644 --- a/lnutil/msglib.go +++ b/lnutil/msglib.go @@ -73,6 +73,11 @@ const ( MSGID_REMOTE_RPCREQUEST = 0xB0 // Contains an RPC request from a remote peer MSGID_REMOTE_RPCRESPONSE = 0xB1 // Contains an RPC response to send to a remote peer + MSGID_CHUNKS_BEGIN uint8 = 0xB2 + MSGID_CHUNK_BODY uint8 = 0xB3 + MSGID_CHUNKS_END uint8 = 0xB4 + + DIGEST_TYPE_SHA256 = 0x00 DIGEST_TYPE_RIPEMD160 = 0x01 ) @@ -1739,6 +1744,9 @@ type DlcOfferAcceptMsg struct { OurFundMultisigPub [33]byte // The Pubkey to be used to in the contract settlement OurPayoutBase [33]byte + //OurRevokePub [33]byte + OurRefundPKH [20]byte + OurrefundTxSig64 [64]byte // The PKH to be paid to in the contract settlement OurPayoutPKH [20]byte // The UTXOs we are using to fund the contract @@ -1760,6 +1768,8 @@ func NewDlcOfferAcceptMsg(contract *DlcContract, msg.OurChangePKH = contract.OurChangePKH msg.OurFundMultisigPub = contract.OurFundMultisigPub msg.OurPayoutBase = contract.OurPayoutBase + msg.OurRefundPKH = contract.OurRefundPKH + msg.OurrefundTxSig64 = contract.OurrefundTxSig64 msg.OurPayoutPKH = contract.OurPayoutPKH msg.SettlementSignatures = signatures return *msg @@ -1785,6 +1795,8 @@ func NewDlcOfferAcceptMsgFromBytes(b []byte, copy(msg.OurChangePKH[:], buf.Next(20)) copy(msg.OurFundMultisigPub[:], buf.Next(33)) copy(msg.OurPayoutBase[:], buf.Next(33)) + copy(msg.OurRefundPKH[:], buf.Next(20)) + copy(msg.OurrefundTxSig64[:], buf.Next(64)) copy(msg.OurPayoutPKH[:], buf.Next(20)) inputCount, _ := wire.ReadVarInt(buf, 0) @@ -1823,6 +1835,8 @@ func (msg DlcOfferAcceptMsg) Bytes() []byte { buf.Write(msg.OurChangePKH[:]) buf.Write(msg.OurFundMultisigPub[:]) buf.Write(msg.OurPayoutBase[:]) + buf.Write(msg.OurRefundPKH[:]) + buf.Write(msg.OurrefundTxSig64[:]) buf.Write(msg.OurPayoutPKH[:]) inputCount := uint64(len(msg.FundingInputs)) @@ -1864,17 +1878,19 @@ type DlcContractAckMsg struct { Idx uint64 // The settlement signatures of the party acknowledging SettlementSignatures []DlcContractSettlementSignature + OurrefundTxSig64 [64]byte } // NewDlcContractAckMsg generates a new DlcContractAckMsg struct based on the // passed contract and signatures func NewDlcContractAckMsg(contract *DlcContract, - signatures []DlcContractSettlementSignature) DlcContractAckMsg { + signatures []DlcContractSettlementSignature, OurrefundTxSig64 [64]byte) DlcContractAckMsg { msg := new(DlcContractAckMsg) msg.PeerIdx = contract.PeerIdx msg.Idx = contract.TheirIdx msg.SettlementSignatures = signatures + msg.OurrefundTxSig64 = OurrefundTxSig64 return *msg } @@ -1903,6 +1919,9 @@ func NewDlcContractAckMsgFromBytes(b []byte, binary.Read(buf, binary.BigEndian, &msg.SettlementSignatures[i].Outcome) copy(msg.SettlementSignatures[i].Signature[:], buf.Next(64)) } + + copy(msg.OurrefundTxSig64[:], buf.Next(64)) + return *msg, nil } @@ -1921,6 +1940,9 @@ func (msg DlcContractAckMsg) Bytes() []byte { binary.Write(&buf, binary.BigEndian, outcome) buf.Write(msg.SettlementSignatures[i].Signature[:]) } + + buf.Write(msg.OurrefundTxSig64[:]) + return buf.Bytes() } @@ -2412,3 +2434,121 @@ func (msg RemoteControlRpcResponseMsg) Peer() uint32 { func (msg RemoteControlRpcResponseMsg) MsgType() uint8 { return MSGID_REMOTE_RPCRESPONSE } + + +// For chunked messages + +type BeginChunksMsg struct { + PeerIdx uint32 + TimeStamp int64 +} + +func NewChunksBeginMsgFromBytes(b []byte, peerIdx uint32) (BeginChunksMsg, error) { + + msg := new(BeginChunksMsg) + buf := bytes.NewBuffer(b[1:]) // get rid of messageType + + msg.PeerIdx = peerIdx + binary.Read(buf, binary.BigEndian, &msg.TimeStamp) + + return *msg, nil +} + +func (msg BeginChunksMsg) Bytes() []byte { + var buf bytes.Buffer + + buf.WriteByte(msg.MsgType()) + binary.Write(&buf, binary.BigEndian, msg.TimeStamp) + + return buf.Bytes() +} + +func (msg BeginChunksMsg) Peer() uint32 { + return msg.PeerIdx +} + +func (msg BeginChunksMsg) MsgType() uint8 { + return MSGID_CHUNKS_BEGIN +} + + +type ChunkMsg struct { + PeerIdx uint32 + TimeStamp int64 + ChunkSize int32 + Data []byte +} + + +func NewChunkMsgFromBytes(b []byte, peerIdx uint32) (ChunkMsg, error) { + + msg := new(ChunkMsg) + + buf := bytes.NewBuffer(b[1:]) // get rid of messageType + + msg.PeerIdx = peerIdx + binary.Read(buf, binary.BigEndian, &msg.TimeStamp) + binary.Read(buf, binary.BigEndian, &msg.ChunkSize) + + msg.Data = make([]byte, msg.ChunkSize) + binary.Read(buf, binary.BigEndian, msg.Data) + + return *msg, nil + +} + + +func (msg ChunkMsg) Bytes() []byte { + var buf bytes.Buffer + + buf.WriteByte(msg.MsgType()) + binary.Write(&buf, binary.BigEndian, msg.TimeStamp) + binary.Write(&buf, binary.BigEndian, msg.ChunkSize) + buf.Write(msg.Data) + + return buf.Bytes() +} + +func (msg ChunkMsg) Peer() uint32 { + return msg.PeerIdx +} + +func (msg ChunkMsg) MsgType() uint8 { + return MSGID_CHUNK_BODY +} + + + +type EndChunksMsg struct { + PeerIdx uint32 + TimeStamp int64 +} + +func NewChunksEndMsgFromBytes(b []byte, peerIdx uint32) (EndChunksMsg, error) { + + + msg := new(EndChunksMsg) + buf := bytes.NewBuffer(b[1:]) // get rid of messageType + + msg.PeerIdx = peerIdx + binary.Read(buf, binary.BigEndian, &msg.TimeStamp) + + return *msg, nil +} + +func (msg EndChunksMsg) Bytes() []byte { + var buf bytes.Buffer + + buf.WriteByte(msg.MsgType()) + binary.Write(&buf, binary.BigEndian, msg.TimeStamp) + + return buf.Bytes() +} + +func (msg EndChunksMsg) Peer() uint32 { + return msg.PeerIdx +} + +func (msg EndChunksMsg) MsgType() uint8 { + return MSGID_CHUNKS_END +} \ No newline at end of file diff --git a/qln/dlc.go b/qln/dlc.go index fd61c7846..caf8c944a 100644 --- a/qln/dlc.go +++ b/qln/dlc.go @@ -2,7 +2,6 @@ package qln import ( "fmt" - "github.com/mit-dci/lit/btcutil" "github.com/mit-dci/lit/btcutil/txscript" "github.com/mit-dci/lit/btcutil/txsort" @@ -13,6 +12,7 @@ import ( "github.com/mit-dci/lit/portxo" "github.com/mit-dci/lit/sig64" "github.com/mit-dci/lit/wire" + "github.com/mit-dci/lit/consts" ) func (nd *LitNode) AddContract() (*lnutil.DlcContract, error) { @@ -41,22 +41,44 @@ func (nd *LitNode) OfferDlc(peerIdx uint32, cIdx uint64) error { var nullBytes [33]byte // Check if everything's set - if c.OracleA == nullBytes { - return fmt.Errorf("You need to set an oracle for the contract before offering it") + + + if c.OraclesNumber == dlc.ORACLESNUMBER_NOT_SET { + return fmt.Errorf("You need to set an oracles number for the contract before offering it") } - if c.OracleR == nullBytes { - return fmt.Errorf("You need to set an R-point for the contract before offering it") + if c.OraclesNumber > consts.MaxOraclesNumber { + return fmt.Errorf("The number of oracles have to be less than 8.") + } + + for o := uint32(0); o < c.OraclesNumber; o++ { + + if c.OracleA[o] == nullBytes { + return fmt.Errorf("You need to set all %d oracls for the contract before offering it", c.OraclesNumber) + } + + if c.OracleR[o] == nullBytes { + return fmt.Errorf("You need to set all %d R-points for the contract before offering it", c.OraclesNumber) + } + } if c.OracleTimestamp == 0 { return fmt.Errorf("You need to set a settlement time for the contract before offering it") } + if c.RefundTimestamp == 0 { + return fmt.Errorf("You need to set a refund time for the contract before offering it") + } + if c.CoinType == dlc.COINTYPE_NOT_SET { 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") } @@ -75,6 +97,7 @@ func (nd *LitNode) OfferDlc(peerIdx uint32, cIdx uint64) error { kg.Step[3] = c.PeerIdx | 1<<31 kg.Step[4] = uint32(c.Idx) | 1<<31 + c.OurFundMultisigPub, err = nd.GetUsePub(kg, UseContractFundMultisig) if err != nil { return err @@ -85,12 +108,24 @@ func (nd *LitNode) OfferDlc(peerIdx uint32, cIdx uint64) error { return err } + ourPayoutPKHKey, err := nd.GetUsePub(kg, UseContractPayoutPKH) + if err != nil { + logging.Errorf("Error while getting our payout pubkey: %s", err.Error()) + c.Status = lnutil.ContractStatusError + nd.DlcManager.SaveContract(c) + return err + } + + copy(c.OurPayoutPKH[:], btcutil.Hash160(ourPayoutPKHKey[:])) + // Fund the contract err = nd.FundContract(c) if err != nil { return err } + wal, _ := nd.SubWallet[c.CoinType] + c.OurRefundPKH, err = wal.NewAdr() msg := lnutil.NewDlcOfferMsg(peerIdx, c) c.Status = lnutil.ContractStatusOfferedByMe @@ -192,6 +227,9 @@ func (nd *LitNode) AcceptDlc(cIdx uint64) error { } copy(c.OurPayoutPKH[:], btcutil.Hash160(ourPayoutPKHKey[:])) + wal, _ := nd.SubWallet[c.CoinType] + c.OurRefundPKH, err = wal.NewAdr() + // Now we can sign the division sigs, err := nd.SignSettlementDivisions(c) if err != nil { @@ -201,6 +239,27 @@ func (nd *LitNode) AcceptDlc(cIdx uint64) error { return } + refundTx, err := lnutil.RefundTx(c) + if err != nil { + logging.Errorf("Error of RefundTx: %s", err.Error()) + c.Status = lnutil.ContractStatusError + nd.DlcManager.SaveContract(c) + return + } + + kg.Step[2] = UseContractFundMultisig + mypriv, err := wal.GetPriv(kg) + //wal, _ := nd.SubWallet[c.CoinType] + + + err = lnutil.SignRefundTx(c, refundTx, mypriv) + if err != nil { + logging.Errorf("Error of SignRefundTx: %s", err.Error()) + c.Status = lnutil.ContractStatusError + nd.DlcManager.SaveContract(c) + return + } + msg := lnutil.NewDlcOfferAcceptMsg(c, sigs) c.Status = lnutil.ContractStatusAccepted @@ -227,6 +286,11 @@ func (nd *LitNode) DlcOfferHandler(msg lnutil.DlcOfferMsg, peer *RemotePeer) { c.OurChangePKH = msg.Contract.TheirChangePKH c.TheirChangePKH = msg.Contract.OurChangePKH c.TheirIdx = msg.Contract.Idx + c.TheirPayoutPKH = msg.Contract.OurPayoutPKH + + //c.TheirRevokePub = msg.Contract.OurRevokePub + + c.TheirRefundPKH = msg.Contract.OurRefundPKH c.Division = make([]lnutil.DlcContractDivision, len(msg.Contract.Division)) for i := 0; i < len(msg.Contract.Division); i++ { @@ -236,9 +300,19 @@ func (nd *LitNode) DlcOfferHandler(msg lnutil.DlcOfferMsg, peer *RemotePeer) { // Copy c.CoinType = msg.Contract.CoinType - c.OracleA = msg.Contract.OracleA - c.OracleR = msg.Contract.OracleR + c.FeePerByte = msg.Contract.FeePerByte + + c.OraclesNumber = msg.Contract.OraclesNumber + + for i:=uint32(0); i < c.OraclesNumber; i++ { + + c.OracleA[i] = msg.Contract.OracleA[i] + c.OracleR[i] = msg.Contract.OracleR[i] + + } + c.OracleTimestamp = msg.Contract.OracleTimestamp + c.RefundTimestamp = msg.Contract.RefundTimestamp err := nd.DlcManager.SaveContract(c) if err != nil { @@ -285,6 +359,9 @@ func (nd *LitNode) DlcAcceptHandler(msg lnutil.DlcOfferAcceptMsg, peer *RemotePe c.TheirPayoutBase = msg.OurPayoutBase c.TheirPayoutPKH = msg.OurPayoutPKH c.TheirIdx = msg.OurIdx + c.TheirRefundPKH = msg.OurRefundPKH + c.TheirrefundTxSig64 = msg.OurrefundTxSig64 + c.Status = lnutil.ContractStatusAccepted err = nd.DlcManager.SaveContract(c) @@ -299,18 +376,45 @@ func (nd *LitNode) DlcAcceptHandler(msg lnutil.DlcOfferAcceptMsg, peer *RemotePe return err } - outMsg := lnutil.NewDlcContractAckMsg(c, sigs) + wal, _ := nd.SubWallet[c.CoinType] + + refundTx, err := lnutil.RefundTx(c) + if err != nil { + logging.Errorf("Error of RefundTx: %s", err.Error()) + c.Status = lnutil.ContractStatusError + nd.DlcManager.SaveContract(c) + return err + } + + + var kg portxo.KeyGen + kg.Depth = 5 + kg.Step[0] = 44 | 1<<31 + kg.Step[1] = c.CoinType | 1<<31 + kg.Step[2] = UseContractFundMultisig + kg.Step[3] = c.PeerIdx | 1<<31 + kg.Step[4] = uint32(c.Idx) | 1<<31 + + mypriv, err := wal.GetPriv(kg) + + err = lnutil.SignRefundTx(c, refundTx, mypriv) + if err != nil { + logging.Errorf("Error of SignRefundTx: %s", err.Error()) + c.Status = lnutil.ContractStatusError + nd.DlcManager.SaveContract(c) + return err + } + + outMsg := lnutil.NewDlcContractAckMsg(c, sigs, c.OurrefundTxSig64) c.Status = lnutil.ContractStatusAcknowledged err = nd.DlcManager.SaveContract(c) if err != nil { return err } - nd.tmpSendLitMsg(outMsg) return nil - } func (nd *LitNode) DlcContractAckHandler(msg lnutil.DlcContractAckMsg, peer *RemotePeer) { @@ -323,6 +427,8 @@ func (nd *LitNode) DlcContractAckHandler(msg lnutil.DlcContractAckMsg, peer *Rem // TODO: Check signatures c.Status = lnutil.ContractStatusAcknowledged + c.TheirSettlementSignatures = msg.SettlementSignatures + c.TheirrefundTxSig64 = msg.OurrefundTxSig64 err = nd.DlcManager.SaveContract(c) if err != nil { @@ -371,7 +477,6 @@ func (nd *LitNode) DlcFundingSigsHandler(msg lnutil.DlcContractFundingSigsMsg, p } wal.SignMyInputs(msg.SignedFundingTx) - wal.DirectSendTx(msg.SignedFundingTx) err = wal.WatchThis(c.FundingOutpoint) @@ -474,18 +579,55 @@ 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) @@ -529,7 +671,7 @@ func (nd *LitNode) FundContract(c *lnutil.DlcContract) error { return nil } -func (nd *LitNode) SettleContract(cIdx uint64, oracleValue int64, oracleSig [32]byte) ([32]byte, [32]byte, error) { +func (nd *LitNode) SettleContract(cIdx uint64, oracleValue int64, oraclesSig[consts.MaxOraclesNumber][32]byte) ([32]byte, [32]byte, error) { c, err := nd.DlcManager.LoadContract(cIdx) if err != nil { @@ -594,6 +736,7 @@ func (nd *LitNode) SettleContract(cIdx uint64, oracleValue int64, oracleSig [32] logging.Errorf("SettleContract FundTxScript err %s", err.Error()) return [32]byte{}, [32]byte{}, err } + // swap if needed if swap { @@ -609,46 +752,121 @@ 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){ + + 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) + + + var pubOracleBytes [][33]byte + + privOracle0, pubOracle0 := koblitz.PrivKeyFromBytes(koblitz.S256(), oraclesSig[0][:]) + privContractOutput := lnutil.CombinePrivateKeys(privSpend, privOracle0) + + var pubOracleBytes0 [33]byte + copy(pubOracleBytes0[:], pubOracle0.SerializeCompressed()) + pubOracleBytes = append(pubOracleBytes, pubOracleBytes0) + + for i:=uint32(1); i < c.OraclesNumber; i++ { + + privOracle, pubOracle := koblitz.PrivKeyFromBytes(koblitz.S256(), oraclesSig[i][:]) + privContractOutput = lnutil.CombinePrivateKeys(privContractOutput, privOracle) + + var pubOracleBytes1 [33]byte + copy(pubOracleBytes1[:], pubOracle.SerializeCompressed()) + pubOracleBytes = append(pubOracleBytes, pubOracleBytes1) + + } + + settleScript := lnutil.DlcCommitScript(c.OurPayoutBase, c.TheirPayoutBase, pubOracleBytes , 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 - // 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) +} + + + +func (nd *LitNode) RefundContract(cIdx uint64) (bool, error) { + + c, err := nd.DlcManager.LoadContract(cIdx) if err != nil { - return [32]byte{}, [32]byte{}, err + logging.Errorf("SettleContract FindContract err %s\n", err.Error()) + return false, err } - return settleTx.TxHash(), txClaim.TxHash(), nil + + wal, _ := nd.SubWallet[c.CoinType] + + refundTx, err := lnutil.RefundTx(c) + myBigSig := sig64.SigDecompress(c.OurrefundTxSig64) + myBigSig = append(myBigSig, byte(txscript.SigHashAll)) + theirBigSig := sig64.SigDecompress(c.TheirrefundTxSig64) + theirBigSig = append(theirBigSig, byte(txscript.SigHashAll)) + pre, swap, err := lnutil.FundTxScript(c.OurFundMultisigPub, c.TheirFundMultisigPub) + + // swap if needed + if swap { + refundTx.TxIn[0].Witness = SpendMultiSigWitStack(pre, theirBigSig, myBigSig) + } else { + refundTx.TxIn[0].Witness = SpendMultiSigWitStack(pre, myBigSig, theirBigSig) + } + + err = wal.DirectSendTx(refundTx) + + return true, nil + } diff --git a/qln/magicnums.go b/qln/magicnums.go index 8fdf7a4be..6ad674cc4 100644 --- a/qln/magicnums.go +++ b/qln/magicnums.go @@ -20,7 +20,9 @@ const ( // key derivation path for contract payout PKH (the hash the contract // pays TO) - UseContractPayoutPKH = 52 | hdkeychain.HardenedKeyStart + UseContractPayoutPKH = 53 | hdkeychain.HardenedKeyStart + + UseContractRevoke = 54 | hdkeychain.HardenedKeyStart // key derivation path for HTLC pubkeys UseHTLCBase = 60 | hdkeychain.HardenedKeyStart 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 diff --git a/test/itest_dlc.py b/test/itest_dlc.py new file mode 100644 index 000000000..69b68da26 --- /dev/null +++ b/test/itest_dlc.py @@ -0,0 +1,790 @@ +import testlib + +import time, datetime +import json + +import pprint + +import requests # pip3 install requests + +import codecs + +deb_mod = False + +def run_t(env, params): + global deb_mod + try: + + lit_funding_amt = params[0] + contract_funding_amt = params[1] + oracles_number = params[2] + oracle_value = params[3] + node_to_settle = params[4] + valueFullyOurs=params[5] + valueFullyTheirs=params[6] + + FundingTxVsize = params[7][0] + SettlementTxVsize = params[7][1] + + feeperbyte = params[8] + + SetTxFeeOurs = params[9] + SetTxFeeTheirs = params[10] + + ClaimTxFeeOurs = params[11] + ClaimTxFeeTheirs = params[12] + + bc = env.bitcoind + + #------------ + # Create oracles + #------------ + + oracles = [] + + for i in range(oracles_number): + env.new_oracle(1, oracle_value) # publishing interval is 1 second. + oracles.append(env.oracles[i]) + + time.sleep(2) + + #------------ + # Create lits + #------------ + + lit1 = env.lits[0] + lit2 = env.lits[1] + + + pp = pprint.PrettyPrinter(indent=4) + + + #------------------------------------------ + if deb_mod: + print("ADDRESSES BEFORE SEND TO ADDRESS") + print("LIT1 Addresses") + print(pp.pprint(lit1.rpc.GetAddresses())) + + print("LIT2 Addresses") + print(pp.pprint(lit2.rpc.GetAddresses())) + + print("bitcoind Addresses") + print(pp.pprint(bc.rpc.listaddressgroupings())) + #------------------------------------------ + + + lit1.connect_to_peer(lit2) + print("---------------") + print('Connecting lit1:', lit1.lnid, 'to lit2:', lit2.lnid) + + addr1 = lit1.make_new_addr() + txid1 = bc.rpc.sendtoaddress(addr1, lit_funding_amt) + + if deb_mod: + print("Funding TxId lit1: " + str(txid1)) + + time.sleep(5) + + addr2 = lit2.make_new_addr() + txid2 = bc.rpc.sendtoaddress(addr2, lit_funding_amt) + + if deb_mod: + print("Funding TxId lit2: " + str(txid2)) + + time.sleep(5) + + env.generate_block() + time.sleep(5) + + bals1 = lit1.get_balance_info() + print('new lit1 balance:', bals1['TxoTotal'], 'in txos,', bals1['ChanTotal'], 'in chans') + bal1sum = bals1['TxoTotal'] + bals1['ChanTotal'] + print(' = sum ', bal1sum) + + print(lit_funding_amt) + + lit_funding_amt *= 100000000 # to satoshi + + bals2 = lit2.get_balance_info() + print('new lit2 balance:', bals2['TxoTotal'], 'in txos,', bals2['ChanTotal'], 'in chans') + bal2sum = bals2['TxoTotal'] + bals2['ChanTotal'] + print(' = sum ', bal2sum) + + + assert bal1sum == lit_funding_amt, "Funding lit1 does not works" + assert bal2sum == lit_funding_amt, "Funding lit2 does not works" + + + #------------------------------------------ + if deb_mod: + print("ADDRESSES AFTER SEND TO ADDRESS") + print("LIT1 Addresses") + print(pp.pprint(lit1.rpc.GetAddresses())) + + print("LIT2 Addresses") + print(pp.pprint(lit2.rpc.GetAddresses())) + + print("bitcoind Addresses") + print(pp.pprint(bc.rpc.listaddressgroupings())) + #------------------------------------------ + + + #------------ + # Add oracles + #------------ + + res = lit1.rpc.ListOracles() + assert len(res) != 0, "Initial lis of oracles must be empty" + + + oracles_pubkey = [] + oidxs = [] + datasources = [] + + for oracle in oracles: + opk = json.loads(oracle.get_pubkey()) + oracles_pubkey.append(opk) + + oidx = lit1.rpc.AddOracle(Key=opk["A"], Name=opk["A"])["Oracle"]["Idx"] + oidxs.append(oidx) + lit2.rpc.AddOracle(Key=opk["A"], Name=opk["A"])["Oracle"]["Idx"] + + datasources.append(json.loads(oracle.get_datasources())) + + + #------------ + # Now we have to create a contract in the lit1 node. + #------------ + + contract = lit1.rpc.NewContract() + + res = lit1.rpc.ListContracts() + assert len(res["Contracts"]) == 1, "ListContracts does not works" + + + res = lit1.rpc.GetContract(Idx=1) + assert res["Contract"]["Idx"] == 1, "GetContract does not works" + + + res = lit1.rpc.SetContractOraclesNumber(CIdx=contract["Contract"]["Idx"], OraclesNumber=oracles_number) + assert res["Success"], "SetContractOraclesNumber does not works" + + res = lit1.rpc.SetContractOracle(CIdx=contract["Contract"]["Idx"], OIdx=oidxs) + assert res["Success"], "SetContractOracle does not works" + + + # Since the oracle publishes data every 1 second (we set this time above), + # we increase the time for a point by 3 seconds. + + settlement_time = int(time.time()) + 3 + + # dlc contract settime + res = lit1.rpc.SetContractSettlementTime(CIdx=contract["Contract"]["Idx"], Time=settlement_time) + assert res["Success"], "SetContractSettlementTime does not works" + + # we set settlement_time equal to refundtime, actually the refund transaction will be valid. + res = lit1.rpc.SetContractRefundTime(CIdx=contract["Contract"]["Idx"], Time=settlement_time) + assert res["Success"], "SetContractRefundTime does not works" + + # we set settlement_time equal to refundtime, actually the refund transaction will be valid. + lit1.rpc.SetContractRefundTime(CIdx=contract["Contract"]["Idx"], Time=settlement_time) + + res = lit1.rpc.ListContracts() + assert res["Contracts"][contract["Contract"]["Idx"] - 1]["OracleTimestamp"] == settlement_time, "SetContractSettlementTime does not match settlement_time" + + + decode_hex = codecs.getdecoder("hex_codec") + brpoints = [] + rpoints = [] + for oracle, datasource in zip(oracles, datasources): + res = oracle.get_rpoint(datasource[0]["id"], settlement_time) + print(res) + b_RPoint = decode_hex(json.loads(res)['R'])[0] + RPoint = [elem for elem in b_RPoint] + brpoints.append(RPoint) + rpoints.append(res) + + + res = lit1.rpc.SetContractRPoint(CIdx=contract["Contract"]["Idx"], RPoint=brpoints) + assert res["Success"], "SetContractRpoint does not works" + + + lit1.rpc.SetContractCoinType(CIdx=contract["Contract"]["Idx"], CoinType = 257) + res = lit1.rpc.GetContract(Idx=contract["Contract"]["Idx"]) + assert res["Contract"]["CoinType"] == 257, "SetContractCoinType does not works" + + + lit1.rpc.SetContractFeePerByte(CIdx=contract["Contract"]["Idx"], FeePerByte = feeperbyte) + res = lit1.rpc.GetContract(Idx=contract["Contract"]["Idx"]) + assert res["Contract"]["FeePerByte"] == feeperbyte, "SetContractFeePerByte does not works" + + ourFundingAmount = contract_funding_amt + theirFundingAmount = contract_funding_amt + + lit1.rpc.SetContractFunding(CIdx=contract["Contract"]["Idx"], OurAmount=ourFundingAmount, TheirAmount=theirFundingAmount) + res = lit1.rpc.GetContract(Idx=contract["Contract"]["Idx"]) + assert res["Contract"]["OurFundingAmount"] == ourFundingAmount, "SetContractFunding does not works" + assert res["Contract"]["TheirFundingAmount"] == theirFundingAmount, "SetContractFunding does not works" + + res = lit1.rpc.SetContractDivision(CIdx=contract["Contract"]["Idx"], ValueFullyOurs=valueFullyOurs, ValueFullyTheirs=valueFullyTheirs) + assert res["Success"], "SetContractDivision does not works" + + time.sleep(5) + + + res = lit1.rpc.ListConnections() + + res = lit1.rpc.OfferContract(CIdx=contract["Contract"]["Idx"], PeerIdx=lit1.get_peer_id(lit2)) + assert res["Success"], "OfferContract does not works" + + time.sleep(5) + + res = lit2.rpc.ContractRespond(AcceptOrDecline=True, CIdx=1) + assert res["Success"], "ContractRespond on lit2 does not works" + + time.sleep(5) + + + if deb_mod: + print("ADDRESSES AFTER CONTRACT RESPOND") + print("LIT1 Addresses") + print(lit1.rpc.GetAddresses()) + + print("LIT2 Addresses") + print(lit2.rpc.GetAddresses()) + + print("bitcoind Addresses") + print(bc.rpc.listaddressgroupings()) + + + env.generate_block() + time.sleep(2) + + print("Accept") + bals1 = lit1.get_balance_info() + print('new lit1 balance:', bals1['TxoTotal'], 'in txos,', bals1['ChanTotal'], 'in chans') + bal1sum = bals1['TxoTotal'] + bals1['ChanTotal'] + print(' = sum ', bal1sum) + + lit1_bal_after_accept = (lit_funding_amt - ourFundingAmount) - (126*feeperbyte) + + + bals2 = lit2.get_balance_info() + print('new lit2 balance:', bals2['TxoTotal'], 'in txos,', bals2['ChanTotal'], 'in chans') + bal2sum = bals2['TxoTotal'] + bals2['ChanTotal'] + print(' = sum ', bal2sum) + + lit2_bal_after_accept = (lit_funding_amt - theirFundingAmount) - (126*feeperbyte) + + + assert bal1sum == lit1_bal_after_accept, "lit1 Balance after contract accept does not match" + assert bal2sum == lit2_bal_after_accept, "lit2 Balance after contract accept does not match" + + + OraclesSig = [] + OraclesVal = [] + + i = 0 + while True: + + publications_result = [] + + for o, r in zip(oracles, rpoints): + publications_result.append(o.get_publication(json.loads(r)['R'])) + + + time.sleep(5) + i += 1 + if i>4: + assert False, "Error: Oracle does not publish data" + + try: + + for pr in publications_result: + oracle_val = json.loads(pr)["value"] + OraclesVal.append(oracle_val) + oracle_sig = json.loads(pr)["signature"] + b_OracleSig = decode_hex(oracle_sig)[0] + OracleSig = [elem for elem in b_OracleSig] + OraclesSig.append(OracleSig) + + break + except BaseException as e: + print(e) + next + + # Oracles have to publish the same value + vEqual = True + nTemp = OraclesVal[0] + for v in OraclesVal: + if nTemp != v: + vEqual = False + break; + assert vEqual, "Oracles publish different values" + + res = env.lits[node_to_settle].rpc.SettleContract(CIdx=contract["Contract"]["Idx"], OracleValue=OraclesVal[0], OracleSig=OraclesSig) + assert res["Success"], "SettleContract does not works." + + + time.sleep(5) + + try: + env.generate_block(1) + time.sleep(1) + env.generate_block(1) + time.sleep(1) + env.generate_block(1) + time.sleep(1) + except BaseException as be: + print("Exception After SettleContract: ") + print(be) + + time.sleep(2) + #------------------------------------------ + + if deb_mod: + + best_block_hash = bc.rpc.getbestblockhash() + bb = bc.rpc.getblock(best_block_hash) + print(bb) + print("bb['height']: " + str(bb['height'])) + + print("Balance from RPC: " + str(bc.rpc.getbalance())) + + # batch support : print timestamps of blocks 0 to 99 in 2 RPC round-trips: + commands = [ [ "getblockhash", height] for height in range(bb['height'] + 1) ] + block_hashes = bc.rpc.batch_(commands) + blocks = bc.rpc.batch_([ [ "getblock", h ] for h in block_hashes ]) + block_times = [ block["time"] for block in blocks ] + print(block_times) + + print('--------------------') + + for b in blocks: + print("--------BLOCK--------") + print(b) + tx = b["tx"] + #print(tx) + try: + + for i in range(len(tx)): + print("--------TRANSACTION--------") + rtx = bc.rpc.getrawtransaction(tx[i]) + print(rtx) + decoded = bc.rpc.decoderawtransaction(rtx) + pp.pprint(decoded) + except BaseException as be: + print(be) + # print(type(rtx)) + print('--------') + + + + if deb_mod: + print("ADDRESSES AFTER SETTLE") + print("LIT1 Addresses") + print(pp.pprint(lit1.rpc.GetAddresses())) + + print("LIT2 Addresses") + print(pp.pprint(lit2.rpc.GetAddresses())) + + print("bitcoind Addresses") + print(pp.pprint(bc.rpc.listaddressgroupings())) + #------------------------------------------ + + + + print("=====START CONTRACT N1=====") + res = lit1.rpc.ListContracts() + #print(pp.pprint(res)) + print(res) + print("=====END CONTRACT N1=====") + + print("=====START CONTRACT N2=====") + res = lit2.rpc.ListContracts() + #print(pp.pprint(res)) + print(res) + print("=====END CONTRACT N2=====") + + + print("ORACLE VALUE:", OraclesVal[0], "; oracle signature:", OraclesVal[0]) + + valueOurs = 0 + + + valueOurs = env.lits[node_to_settle].rpc.GetContractDivision(CIdx=contract["Contract"]["Idx"],OracleValue=OraclesVal[0])['ValueOurs'] + valueTheirs = contract_funding_amt * 2 - valueOurs + + print("valueOurs:", valueOurs, "; valueTheirs:", valueTheirs) + + + lit1_bal_after_settle = valueOurs - SetTxFeeOurs + lit2_bal_after_settle = valueTheirs - SetTxFeeTheirs + + + lit1_bal_after_claim = lit1_bal_after_settle - ClaimTxFeeOurs + lit2_bal_after_claim = lit2_bal_after_settle - ClaimTxFeeTheirs + + lit1_bal_result = lit1_bal_after_claim + lit1_bal_after_accept + lit2_bal_result = lit2_bal_after_claim + lit2_bal_after_accept + + print("============== Fees Calc ===========================") + print("lit1_bal_after_settle", lit1_bal_after_settle) + print("lit2_bal_after_settle", lit2_bal_after_settle) + + print("lit1_bal_after_claim",lit1_bal_after_claim) + print("lit2_bal_after_claim",lit2_bal_after_claim) + + print("lit1_bal_result: ", lit1_bal_result) + print("lit2_bal_result: ", lit2_bal_result) + print("====================================================") + + + + bals1 = lit1.get_balance_info() + print('new lit1 balance:', bals1['TxoTotal'], 'in txos,', bals1['ChanTotal'], 'in chans') + bal1sum = bals1['TxoTotal'] + bals1['ChanTotal'] + print(bals1) + print(' = sum ', bal1sum) + + + bals2 = lit2.get_balance_info() + print('new lit2 balance:', bals2['TxoTotal'], 'in txos,', bals2['ChanTotal'], 'in chans') + bal2sum = bals2['TxoTotal'] + bals2['ChanTotal'] + print(bals2) + print(' = sum ', bal2sum) + + if node_to_settle == 0: + assert bal1sum == lit1_bal_result, "The resulting lit1 node balance does not match." + assert bal2sum == lit2_bal_result, "The resulting lit2 node balance does not match." + elif node_to_settle == 1: + assert bal1sum == lit2_bal_result, "The resulting lit1 node balance does not match." + assert bal2sum == lit1_bal_result, "The resulting lit2 node balance does not match." + + + + except BaseException as be: + raise be + + + +def t_11_0(env): + + #----------------------------- + # 1)Funding transaction. + # Here can be a situation when peers have different number of inputs. + + # Vsize from Blockchain: 252 + + # So we expect lit1, and lit2 balances equal to 89989920 !!! + # 90000000 - 89989920 = 10080 + # But this is only when peers have one input each. What we expect. + + #----------------------------- + # 2) SettlementTx vsize will be printed + + + # Vsize from Blockchain: 181 + + # There fore we expect here + # valueOurs: 17992800 = 18000000 - 7200 !!! + # valueTheirs: 1992800 = 2000000 - 7200 !!! + + #----------------------------- + + # 3) Claim TX in SettleContract + # Here the transaction vsize is always the same: 121 + + + # Vsize from Blockchain: 121 + + #----------------------------- + + # 4) Claim TX from another peer + # Here the transaction vsize is always the same: 110 + + # Vsize from Blockchain: 110 + + #----------------------------- + + # 17992800 - (121 * 80) = 17983120 + # 89989920 + 17983120 = 107973040 + + # 1992800 - (110*80) = 1984000 + # 89989920 + 1984000 = 91973920 + + #----------------------------- + + # AFter Settle + # new lit1 balance: 107973040 in txos, 0 in chans + # = sum 107973040 + # {'CoinType': 257, 'SyncHeight': 514, 'ChanTotal': 0, 'TxoTotal': 107973040, 'MatureWitty': 107973040, 'FeeRate': 80} + # new lit2 balance: 91973920 in txos, 0 in chans + # = sum 91973920 + + #----------------------------- + + oracles_number = 1 + oracle_value = 11 + node_to_settle = 0 + + valueFullyOurs=10 + valueFullyTheirs=20 + + lit_funding_amt = 1 # 1 BTC + contract_funding_amt = 10000000 # satoshi + + FundingTxVsize = 252 + SettlementTxVsize = 180 + + SetTxFeeOurs = 7200 + SetTxFeeTheirs = 7200 + + ClaimTxFeeOurs = 121 * 80 + ClaimTxFeeTheirs = 110 * 80 + + + feeperbyte = 80 + + + vsizes = [FundingTxVsize, SettlementTxVsize] + + params = [lit_funding_amt, contract_funding_amt, oracles_number, oracle_value, node_to_settle, valueFullyOurs, valueFullyTheirs, vsizes, feeperbyte, SetTxFeeOurs, SetTxFeeTheirs, ClaimTxFeeOurs, ClaimTxFeeTheirs] + + run_t(env, params) + + + +# ----------------------------------------------------------------------------- + + +def t_1300_1(env): + + #----------------------------- + # 1)Funding transaction. + # Here can be a situation when peers have different number of inputs. + + # Vsize from Blockchain: 252 + + # So we expect lit1, and lit2 balances equal to 89989920 + # 90000000 - 89989920 = 10080 + # But this is only when peers have one input each. What we expect. + + #----------------------------- + # 2) SettlementTx vsize will be printed + + + # Vsize from Blockchain: 181 + + # There fore we expect here + # valueOurs: 5992800 = 6000000 - 7200 !!! + # valueTheirs: 13992800 = 14000000 - 7200 !!! + + #----------------------------- + + # 3) Claim TX in SettleContract lit1 + # Here the transaction vsize is always the same: 121 + + # Vsize from Blockchain: 121 + + #----------------------------- + + # 4) Claim TX from another peer lit0 + # Here the transaction vsize is always the same: 110 + + # Vsize from Blockchain: 110 + + #----------------------------- + + # 5992800 - (121 * 80) = 5983120 + # 89989920 + 5983120 = 95973040 + + # 13992800 - (110*80) = 13984000 + # 89989920 + 13984000 = 103973920 + + #----------------------------- + + # AFter Settle + # new lit1 balance: 103973920 in txos, 0 in chans + # = sum 103973920 + + + # new lit2 balance: 95973040 in txos, 0 in chans + # = sum 95973040 + + #----------------------------- + + oracles_number = 3 + oracle_value = 1300 + node_to_settle = 1 + + valueFullyOurs=1000 + valueFullyTheirs=2000 + + lit_funding_amt = 1 # 1 BTC + contract_funding_amt = 10000000 # satoshi + + FundingTxVsize = 252 + SettlementTxVsize = 180 + + + SetTxFeeOurs = 7200 + SetTxFeeTheirs = 7200 + + ClaimTxFeeOurs = 121 * 80 + ClaimTxFeeTheirs = 110 * 80 + + + feeperbyte = 80 + + + vsizes = [FundingTxVsize, SettlementTxVsize] + + params = [lit_funding_amt, contract_funding_amt, oracles_number, oracle_value, node_to_settle, valueFullyOurs, valueFullyTheirs, vsizes, feeperbyte, SetTxFeeOurs, SetTxFeeTheirs, ClaimTxFeeOurs, ClaimTxFeeTheirs] + + run_t(env, params) + + + +# ----------------------------------------------------------------------------- + + +def t_10_0(env): + + oracles_number = 3 + oracle_value = 10 + node_to_settle = 0 + + valueFullyOurs=10 + valueFullyTheirs=20 + + lit_funding_amt = 1 # 1 BTC + contract_funding_amt = 10000000 # satoshi + + FundingTxVsize = 252 + SettlementTxVsize = 150 + + SetTxFeeOurs = 150 * 80 + SetTxFeeTheirs = 0 + + ClaimTxFeeOurs = 121 * 80 + ClaimTxFeeTheirs = 0 + + + feeperbyte = 80 + + + vsizes = [FundingTxVsize, SettlementTxVsize] + + params = [lit_funding_amt, contract_funding_amt, oracles_number, oracle_value, node_to_settle, valueFullyOurs, valueFullyTheirs, vsizes, feeperbyte, SetTxFeeOurs, SetTxFeeTheirs, ClaimTxFeeOurs, ClaimTxFeeTheirs] + + run_t(env, params) + + + +# ----------------------------------------------------------------------------- + + + +def t_10_1(env): + + oracles_number = 3 + oracle_value = 10 + node_to_settle = 1 + + valueFullyOurs=10 + valueFullyTheirs=20 + + lit_funding_amt = 1 # 1 BTC + contract_funding_amt = 10000000 # satoshi + + FundingTxVsize = 252 + SettlementTxVsize = 137 + + + + SetTxFeeOurs = 0 + SetTxFeeTheirs = 137 * 80 + + ClaimTxFeeOurs = 0 + ClaimTxFeeTheirs = 110 * 80 + + + feeperbyte = 80 + + + vsizes = [FundingTxVsize, SettlementTxVsize] + + params = [lit_funding_amt, contract_funding_amt, oracles_number, oracle_value, node_to_settle, valueFullyOurs, valueFullyTheirs, vsizes, feeperbyte, SetTxFeeOurs, SetTxFeeTheirs, ClaimTxFeeOurs, ClaimTxFeeTheirs] + + run_t(env, params) + + +# ----------------------------------------------------------------------------- + +def t_20_0(env): + + + oracles_number = 3 + oracle_value = 20 + node_to_settle = 0 + + valueFullyOurs=10 + valueFullyTheirs=20 + + lit_funding_amt = 1 # 1 BTC + contract_funding_amt = 10000000 # satoshi + + FundingTxVsize = 252 + SettlementTxVsize = 137 + + SetTxFeeOurs = 0 + SetTxFeeTheirs = 137 * 80 + + ClaimTxFeeOurs = 0 + ClaimTxFeeTheirs = 110 * 80 + + + feeperbyte = 80 + + + vsizes = [FundingTxVsize, SettlementTxVsize] + + params = [lit_funding_amt, contract_funding_amt, oracles_number, oracle_value, node_to_settle, valueFullyOurs, valueFullyTheirs, vsizes, feeperbyte, SetTxFeeOurs, SetTxFeeTheirs, ClaimTxFeeOurs, ClaimTxFeeTheirs] + + run_t(env, params) + + +# ----------------------------------------------------------------------------- + +def t_20_1(env): + + + oracles_number = 3 + oracle_value = 20 + node_to_settle = 1 + + valueFullyOurs=10 + valueFullyTheirs=20 + + lit_funding_amt = 1 # 1 BTC + contract_funding_amt = 10000000 # satoshi + + FundingTxVsize = 252 + SettlementTxVsize = 150 + + + + SetTxFeeOurs = 150 * 80 + SetTxFeeTheirs = 0 + + ClaimTxFeeOurs = 121 * 80 + ClaimTxFeeTheirs = 0 + + feeperbyte = 80 + + vsizes = [FundingTxVsize, SettlementTxVsize] + + params = [lit_funding_amt, contract_funding_amt, oracles_number, oracle_value, node_to_settle, valueFullyOurs, valueFullyTheirs, vsizes, feeperbyte, SetTxFeeOurs, SetTxFeeTheirs, ClaimTxFeeOurs, ClaimTxFeeTheirs] + + run_t(env, params) diff --git a/test/itest_dlcrefund.py b/test/itest_dlcrefund.py new file mode 100644 index 000000000..89d419853 --- /dev/null +++ b/test/itest_dlcrefund.py @@ -0,0 +1,428 @@ +import testlib + +import time, datetime +import json + +import pprint + +import requests # pip3 install requests + +import codecs + +deb_mod = False + + +def run_t(env, params): + global deb_mod + try: + + lit_funding_amt = params[0] + contract_funding_amt = params[1] + oracles_number = params[2] + oracle_value = params[3] + valueFullyOurs=params[4] + valueFullyTheirs=params[5] + + feeperbyte = params[6] + + node_to_refund = params[7] + + bc = env.bitcoind + + #------------ + # Create oracles + #------------ + + oracles = [] + + for i in range(oracles_number): + env.new_oracle(1, oracle_value) # publishing interval is 1 second. + oracles.append(env.oracles[i]) + + time.sleep(2) + + #------------ + # Create lits + #------------ + + lit1 = env.lits[0] + lit2 = env.lits[1] + + + pp = pprint.PrettyPrinter(indent=4) + + + #------------------------------------------ + if deb_mod: + print("ADDRESSES BEFORE SEND TO ADDRESS") + print("LIT1 Addresses") + print(pp.pprint(lit1.rpc.GetAddresses())) + + print("LIT2 Addresses") + print(pp.pprint(lit2.rpc.GetAddresses())) + + print("bitcoind Addresses") + print(pp.pprint(bc.rpc.listaddressgroupings())) + #------------------------------------------ + + + lit1.connect_to_peer(lit2) + print("---------------") + print('Connecting lit1:', lit1.lnid, 'to lit2:', lit2.lnid) + + addr1 = lit1.make_new_addr() + txid1 = bc.rpc.sendtoaddress(addr1, lit_funding_amt) + + if deb_mod: + print("Funding TxId lit1: " + str(txid1)) + + time.sleep(2) + + addr2 = lit2.make_new_addr() + txid2 = bc.rpc.sendtoaddress(addr2, lit_funding_amt) + + if deb_mod: + print("Funding TxId lit2: " + str(txid2)) + + time.sleep(2) + + env.generate_block() + time.sleep(2) + + + #------------------------------------------ + if deb_mod: + print("ADDRESSES AFTER SEND TO ADDRESS") + print("LIT1 Addresses") + print(pp.pprint(lit1.rpc.GetAddresses())) + + print("LIT2 Addresses") + print(pp.pprint(lit2.rpc.GetAddresses())) + + print("bitcoind Addresses") + print(pp.pprint(bc.rpc.listaddressgroupings())) + #------------------------------------------ + + + print("Funding") + bals1 = lit1.get_balance_info() + print('new lit1 balance:', bals1['TxoTotal'], 'in txos,', bals1['ChanTotal'], 'in chans') + bal1sum = bals1['TxoTotal'] + bals1['ChanTotal'] + print(' = sum ', bal1sum) + + print(lit_funding_amt) + + lit_funding_amt *= 100000000 # to satoshi + + + bals2 = lit2.get_balance_info() + print('new lit2 balance:', bals2['TxoTotal'], 'in txos,', bals2['ChanTotal'], 'in chans') + bal2sum = bals2['TxoTotal'] + bals2['ChanTotal'] + print(' = sum ', bal2sum) + + + assert bal1sum == lit_funding_amt, "Funding lit1 does not works" + assert bal2sum == lit_funding_amt, "Funding lit2 does not works" + + # #------------ + # # Add oracles + # #------------ + + res = lit1.rpc.ListOracles() + assert len(res) != 0, "Initial lis of oracles must be empty" + + oracles_pubkey = [] + oidxs = [] + datasources = [] + + for oracle in oracles: + opk = json.loads(oracle.get_pubkey()) + oracles_pubkey.append(opk) + + oidx = lit1.rpc.AddOracle(Key=opk["A"], Name=opk["A"])["Oracle"]["Idx"] + oidxs.append(oidx) + lit2.rpc.AddOracle(Key=opk["A"], Name=opk["A"])["Oracle"]["Idx"] + + datasources.append(json.loads(oracle.get_datasources())) + + + # #------------ + # # Now we have to create a contract in the lit1 node. + # #------------ + + contract = lit1.rpc.NewContract() + + res = lit1.rpc.ListContracts() + assert len(res["Contracts"]) == 1, "ListContracts does not works" + + + res = lit1.rpc.GetContract(Idx=1) + assert res["Contract"]["Idx"] == 1, "GetContract does not works" + + + res = lit1.rpc.SetContractOraclesNumber(CIdx=contract["Contract"]["Idx"], OraclesNumber=oracles_number) + assert res["Success"], "SetContractOraclesNumber does not works" + + res = lit1.rpc.SetContractOracle(CIdx=contract["Contract"]["Idx"], OIdx=oidxs) + assert res["Success"], "SetContractOracle does not works" + + + # Since the oracle publishes data every 1 second (we set this time above), + # we increase the time for a point by 3 seconds. + + settlement_time = int(time.time()) + 3 + + # dlc contract settime + res = lit1.rpc.SetContractSettlementTime(CIdx=contract["Contract"]["Idx"], Time=settlement_time) + assert res["Success"], "SetContractSettlementTime does not works" + + # we set settlement_time equal to refundtime, actually the refund transaction will be valid. + res = lit1.rpc.SetContractRefundTime(CIdx=contract["Contract"]["Idx"], Time=settlement_time) + assert res["Success"], "SetContractRefundTime does not works" + + res = lit1.rpc.ListContracts() + assert res["Contracts"][contract["Contract"]["Idx"] - 1]["OracleTimestamp"] == settlement_time, "SetContractSettlementTime does not match settlement_time" + + decode_hex = codecs.getdecoder("hex_codec") + brpoints = [] + rpoints = [] + for oracle, datasource in zip(oracles, datasources): + res = oracle.get_rpoint(datasource[0]["id"], settlement_time) + print(res) + b_RPoint = decode_hex(json.loads(res)['R'])[0] + RPoint = [elem for elem in b_RPoint] + brpoints.append(RPoint) + rpoints.append(res) + + res = lit1.rpc.SetContractRPoint(CIdx=contract["Contract"]["Idx"], RPoint=brpoints) + assert res["Success"], "SetContractRpoint does not works" + + lit1.rpc.SetContractCoinType(CIdx=contract["Contract"]["Idx"], CoinType = 257) + res = lit1.rpc.GetContract(Idx=contract["Contract"]["Idx"]) + assert res["Contract"]["CoinType"] == 257, "SetContractCoinType does not works" + + + lit1.rpc.SetContractFeePerByte(CIdx=contract["Contract"]["Idx"], FeePerByte = feeperbyte) + res = lit1.rpc.GetContract(Idx=contract["Contract"]["Idx"]) + assert res["Contract"]["FeePerByte"] == feeperbyte, "SetContractFeePerByte does not works" + + ourFundingAmount = contract_funding_amt + theirFundingAmount = contract_funding_amt + + lit1.rpc.SetContractFunding(CIdx=contract["Contract"]["Idx"], OurAmount=ourFundingAmount, TheirAmount=theirFundingAmount) + res = lit1.rpc.GetContract(Idx=contract["Contract"]["Idx"]) + assert res["Contract"]["OurFundingAmount"] == ourFundingAmount, "SetContractFunding does not works" + assert res["Contract"]["TheirFundingAmount"] == theirFundingAmount, "SetContractFunding does not works" + + res = lit1.rpc.SetContractDivision(CIdx=contract["Contract"]["Idx"], ValueFullyOurs=valueFullyOurs, ValueFullyTheirs=valueFullyTheirs) + assert res["Success"], "SetContractDivision does not works" + + time.sleep(3) + + res = lit1.rpc.ListConnections() + print(res) + + res = lit1.rpc.OfferContract(CIdx=contract["Contract"]["Idx"], PeerIdx=lit1.get_peer_id(lit2)) + assert res["Success"], "OfferContract does not works" + + time.sleep(3) + + res = lit2.rpc.ContractRespond(AcceptOrDecline=True, CIdx=1) + assert res["Success"], "ContractRespond on lit2 does not works" + + time.sleep(3) + + #------------------------------------------ + + if deb_mod: + print("ADDRESSES AFTER CONTRACT RESPOND") + print("LIT1 Addresses") + print(pp.pprint(lit1.rpc.GetAddresses())) + + print("LIT2 Addresses") + print(pp.pprint(lit2.rpc.GetAddresses())) + + print("bitcoind Addresses") + print(pp.pprint(bc.rpc.listaddressgroupings())) + + + # #------------------------------------------ + + env.generate_block() + time.sleep(2) + + + print("Accept") + bals1 = lit1.get_balance_info() + print('new lit1 balance:', bals1['TxoTotal'], 'in txos,', bals1['ChanTotal'], 'in chans') + bal1sum = bals1['TxoTotal'] + bals1['ChanTotal'] + print(' = sum ', bal1sum) + + lit1_bal_after_accept = (lit_funding_amt - ourFundingAmount) - (126*feeperbyte) + + + bals2 = lit2.get_balance_info() + print('new lit2 balance:', bals2['TxoTotal'], 'in txos,', bals2['ChanTotal'], 'in chans') + bal2sum = bals2['TxoTotal'] + bals2['ChanTotal'] + print(' = sum ', bal2sum) + + lit2_bal_after_accept = (lit_funding_amt - theirFundingAmount) - (126*feeperbyte) + + + assert bal1sum == lit1_bal_after_accept, "lit1 Balance after contract accept does not match" + assert bal2sum == lit2_bal_after_accept, "lit2 Balance after contract accept does not match" + + time.sleep(2) + + res = env.lits[node_to_refund].rpc.RefundContract(CIdx=1) + + time.sleep(2) + env.generate_block() + time.sleep(1) + env.generate_block() + time.sleep(1) + + + if deb_mod: + print("ADDRESSES AFTER CONTRACT Refund") + print("LIT1 Addresses") + print(pp.pprint(lit1.rpc.GetAddresses())) + + print("LIT2 Addresses") + print(pp.pprint(lit2.rpc.GetAddresses())) + + print("bitcoind Addresses") + print(pp.pprint(bc.rpc.listaddressgroupings())) + + + + print("Refund") + bals1 = lit1.get_balance_info() + print('new lit1 balance:', bals1['TxoTotal'], 'in txos,', bals1['ChanTotal'], 'in chans') + bal1sum = bals1['TxoTotal'] + bals1['ChanTotal'] + print(' = sum ', bal1sum) + + + bals2 = lit2.get_balance_info() + print('new lit2 balance:', bals2['TxoTotal'], 'in txos,', bals2['ChanTotal'], 'in chans') + bal2sum = bals2['TxoTotal'] + bals2['ChanTotal'] + print(' = sum ', bal2sum) + + assert bal1sum == 99976400, "lit1 After Refund balance does not match." + assert bal2sum == 99976400, "lit2 After Refund balance does not match." + + + #----------------------------------------------- + # Send 10000 sat from lit1 to lit2 + + print("Address to send: ") + print(lit2.rpc.GetAddresses()['WitAddresses'][0]) + + res = lit1.rpc.Send(DestAddrs=[lit2.rpc.GetAddresses()['WitAddresses'][0]], Amts=[99960240]) + + time.sleep(1) + env.generate_block() + time.sleep(2) + + print("After Spend") + bals1 = lit1.get_balance_info() + print('new lit1 balance:', bals1['TxoTotal'], 'in txos,', bals1['ChanTotal'], 'in chans') + bal1sum = bals1['TxoTotal'] + bals1['ChanTotal'] + print(' = sum ', bal1sum) + + + bals2 = lit2.get_balance_info() + print('new lit2 balance:', bals2['TxoTotal'], 'in txos,', bals2['ChanTotal'], 'in chans') + bal2sum = bals2['TxoTotal'] + bals2['ChanTotal'] + print(' = sum ', bal2sum) + + assert bal1sum == 0, "lit1 After Spend balance does not match." + assert bal2sum == 199936640, "lit2 After Spend balance does not match." + + #------------------------------------------------ + + if deb_mod: + + print("==========================================================") + print('Print Blockchain Info') + print("==========================================================") + + + best_block_hash = bc.rpc.getbestblockhash() + bb = bc.rpc.getblock(best_block_hash) + print(bb) + print("bb['height']: " + str(bb['height'])) + + print("Balance from RPC: " + str(bc.rpc.getbalance())) + + # batch support : print timestamps of blocks 0 to 99 in 2 RPC round-trips: + commands = [ [ "getblockhash", height] for height in range(bb['height'] + 1) ] + block_hashes = bc.rpc.batch_(commands) + blocks = bc.rpc.batch_([ [ "getblock", h ] for h in block_hashes ]) + block_times = [ block["time"] for block in blocks ] + print(block_times) + + print('--------------------') + + for b in blocks: + print("--------BLOCK--------") + print(b) + tx = b["tx"] + try: + + for i in range(len(tx)): + print("--------TRANSACTION--------") + rtx = bc.rpc.getrawtransaction(tx[i]) + print(rtx) + decoded = bc.rpc.decoderawtransaction(rtx) + pp.pprint(decoded) + except BaseException as be: + print(be) + print('--------') + + except BaseException as be: + raise be + + +# ==================================================================================== +# ==================================================================================== + + + +def forward(env): + + oracles_number = 3 + oracle_value = 10 + node_to_settle = 0 + + valueFullyOurs=10 + valueFullyTheirs=20 + + lit_funding_amt = 1 # 1 BTC + contract_funding_amt = 10000000 # satoshi + + feeperbyte = 80 + + params = [lit_funding_amt, contract_funding_amt, oracles_number, oracle_value, valueFullyOurs, valueFullyTheirs, feeperbyte, 0] + + run_t(env, params) + + + +def reverse(env): + + oracles_number = 3 + oracle_value = 10 + node_to_settle = 0 + + valueFullyOurs=10 + valueFullyTheirs=20 + + lit_funding_amt = 1 # 1 BTC + contract_funding_amt = 10000000 # satoshi + + feeperbyte = 80 + + params = [lit_funding_amt, contract_funding_amt, oracles_number, oracle_value, valueFullyOurs, valueFullyTheirs, feeperbyte, 1] + + run_t(env, params) \ No newline at end of file diff --git a/test/itest_send.py b/test/itest_send.py index 780ddb93b..fcc896f1f 100644 --- a/test/itest_send.py +++ b/test/itest_send.py @@ -38,5 +38,5 @@ def run_test(env): # Validate. bal2 = bc.rpc.getbalance() print('bitcoind balance:', bal0, '->', bal1, '->', bal2, '(', bal2 - bal1, ')') - if bal2 != bal1 + 12.5 + 0.5: # the 12.5 is because we mined a block + if float(bal2) != float(bal1) + 12.5 + 0.5: # the 12.5 is because we mined a block raise AssertionError("Balance in bitcoind doesn't match what we think it should be!") diff --git a/test/runtests.py b/test/runtests.py index 441c6d656..2f1303e1b 100755 --- a/test/runtests.py +++ b/test/runtests.py @@ -73,7 +73,7 @@ def load_tests_from_file(path): 'name': tname, 'pretty_name': pretty, 'test_func': tfn, - 'node_cnt': t['node_cnt'] + 'node_cnt': t['node_cnt'], }) return tests diff --git a/test/testlib.py b/test/testlib.py index e1d55757f..7d96d948d 100644 --- a/test/testlib.py +++ b/test/testlib.py @@ -6,13 +6,22 @@ import logging import random import shutil +import json import testutil -import btcrpc import litrpc +import requests + + +from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException + + +# The dlcoracle binary must be accessible throught a PATH variable. LIT_BIN = "%s/../lit" % paths.abspath(paths.dirname(__file__)) +ORACLE_BIN = "%s/../dlcoracle" % paths.abspath(paths.dirname(__file__)) + REGTEST_COINTYPE = 257 logger = logging.getLogger("testframework") @@ -53,6 +62,7 @@ def get_new_id(): next_id += 1 return id + class LitNode(): def __init__(self, bcnode): self.bcnode = bcnode @@ -68,7 +78,7 @@ def __init__(self, bcnode): s = '' for _ in range(64): s += hexchars[random.randint(0, len(hexchars) - 1)] - print('Using key:', s) + print('Using key (lit):', s) f.write(s + "\n") # Go and do the initial startup and sync. @@ -88,7 +98,7 @@ def start(self): # Now figure out the args to use and then start Lit. args = [ LIT_BIN, - "-vv", + #"-vv", "--reg", "127.0.0.1:" + str(self.bcnode.p2p_port), "--tn3", "", # disable autoconnect "--dir", self.data_dir, @@ -119,6 +129,7 @@ def get_sync_height(self): for bal in self.rpc.balance(): if bal['CoinType'] == REGTEST_COINTYPE: return bal['SyncHeight'] + print("return -1") return -1 def connect_to_peer(self, other): @@ -194,15 +205,19 @@ def __init__(self): "-rpcuser=rpcuser", "-rpcpassword=rpcpass", "-rpcport=" + str(self.rpc_port), + "-txindex" ] + self.proc = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + # Make the RPC client for it. testutil.wait_until_port("localhost", self.rpc_port) testutil.wait_until_port("localhost", self.p2p_port) - self.rpc = btcrpc.BtcClient("localhost", self.rpc_port, "rpcuser", "rpcpass") + + self.rpc = AuthServiceProxy("http://%s:%s@127.0.0.1:%s"%("rpcuser", "rpcpass", self.rpc_port), timeout=240) # Make sure that we're actually ready to accept RPC calls. def ck_ready(): @@ -234,10 +249,85 @@ def shutdown(self): else: pass # do nothing I guess? +class OracleNode(): + def __init__(self, interval, valueToPublish): + + self.data_dir = new_data_dir("oracle") + self.httpport = str(new_port()) + self.interval = str(interval) + self.valueToPublish = str(valueToPublish) + + # Write a hexkey to the privkey file + with open(paths.join(self.data_dir, "privkey.hex"), 'w+') as f: + s = '' + for _ in range(192): + s += hexchars[random.randint(0, len(hexchars) - 1)] + print('Using key (oracle):', s) + f.write(s + "\n") + + self.start() + + + def start(self): + + # See if we should print stdout + outputredir = subprocess.DEVNULL + ev_output_show = os.getenv("ORACLE_OUTPUT_SHOW", default="0") + if ev_output_show == "1": + outputredir = None + + # Now figure out the args to use and then start Lit. + args = [ + ORACLE_BIN, + "--DataDir="+self.data_dir, + "--HttpPort=" + self.httpport, + "--Interval=" + self.interval, + "--ValueToPublish=" + self.valueToPublish, + ] + + penv = os.environ.copy() + + self.proc = subprocess.Popen(args, + stdin=subprocess.DEVNULL, + stdout=outputredir, + stderr=outputredir, + env=penv) + + def shutdown(self): + if self.proc is not None: + self.proc.kill() + self.proc.wait() + self.proc = None + else: + pass # do nothing I guess? + + shutil.rmtree(self.data_dir) + + def get_pubkey(self): + res = requests.get("http://localhost:"+self.httpport+"/api/pubkey") + return res.text + + def get_datasources(self): + res = requests.get("http://localhost:"+self.httpport+"/api/datasources") + return res.text + + + def get_rpoint(self, datasourceID, unixTime): + res = requests.get("http://localhost:"+self.httpport+"/api/rpoint/" + str(datasourceID) + "/" + str(unixTime)) + print("get_rpoint:", "http://localhost:"+self.httpport+"/api/rpoint/" + str(datasourceID) + "/" + str(unixTime)) + print(res.text) + return res.text + + def get_publication(self, rpoint): + res = requests.get("http://localhost:"+self.httpport+"/api/publication/" + rpoint) + return res.text + + class TestEnv(): def __init__(self, litcnt): logger.info("starting nodes...") self.bitcoind = BitcoinNode() + self.oracles = [] self.lits = [] for i in range(litcnt): node = LitNode(self.bitcoind) @@ -260,6 +350,12 @@ def new_lit_node(self): self.generate_block(count=0) # Force it to wait for sync. return node + def new_oracle(self, interval, valueToPublish): + oracle = OracleNode(interval, valueToPublish) + self.oracles.append(oracle) + return oracle + + def generate_block(self, count=1): if count > 0: self.bitcoind.rpc.generate(count) @@ -279,6 +375,8 @@ def shutdown(self): for l in self.lits: l.shutdown() self.bitcoind.shutdown() + for o in self.oracles: + o.shutdown() def clean_data_dir(): datadir = get_root_data_dir() diff --git a/test/tests.txt b/test/tests.txt index 08fc62242..1d47daa54 100644 --- a/test/tests.txt +++ b/test/tests.txt @@ -23,3 +23,30 @@ pushbreak 2 forward pushbreak 2 reverse pushclose 2 forward pushclose 2 reverse + +# DLC subsystem tx sizes calculation, etc... + +# regular contract +dlc 2 t_11_0 + +# large contract to test updated messaging subsystem +# and also to test settlement from counterparty +# TODO. Fix Error: [Errno 32] Broken pipe +# when delay betwen requests becomes large +#dlc 2 t_1300_1 + +# test at left edge +dlc 2 t_10_0 + +# test at left edge from the counterparty +dlc 2 t_10_1 + +# test at right edge +dlc 2 t_20_0 + +# test at right edge from the counterparty +dlc 2 t_20_1 + +dlcrefund 2 forward +dlcrefund 2 reverse +