From 2dddc3f474c4b59970d88bb745a27c3c3e812949 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Thu, 1 Aug 2024 14:09:25 +0200 Subject: [PATCH 01/98] Publish branch --- .vscode/launch.json | 2 +- CHANGELOG.md | 4 + cmd/psweb/liquid/elements.go | 302 ++++++++++++++++++++++++++++++++++- cmd/psweb/main.go | 67 +++++++- 4 files changed, 372 insertions(+), 3 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 06ff138..5278380 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,7 @@ "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - //"args": ["-datadir", "/home/vlad/.peerswap2"] + // "args": ["-datadir", "/home/vlad/.peerswap2"] } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c53310..a7ec302 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Versions +## 1.7.0 + +- + ## 1.6.8 - AutoFees: do not change inbound fee during pending HTLCs diff --git a/cmd/psweb/liquid/elements.go b/cmd/psweb/liquid/elements.go index deb82e0..d6f065e 100644 --- a/cmd/psweb/liquid/elements.go +++ b/cmd/psweb/liquid/elements.go @@ -379,7 +379,7 @@ func ClaimPegin(rawTx, proof, claimScript string) (string, error) { } func toBitcoin(amountSats uint64) float64 { - return float64(amountSats) / float64(100000000) + return float64(amountSats) / float64(100_000_000) } type MemPoolInfo struct { @@ -416,3 +416,303 @@ func EstimateFee() float64 { return math.Round(result.MemPoolMinFee*100_000_000) / 1000 } + +// identifies if this version of Elements Core supports discounted vSize +func HasDiscountedvSize() bool { + client := ElementsClient() + service := &Elements{client} + wallet := config.Config.ElementsWallet + params := &[]string{} + + r, err := service.client.call("getnetworkinfo", params, "/wallet/"+wallet) + if err = handleError(err, &r); err != nil { + log.Printf("getnetworkinfo: %v", err) + return false + } + + var response map[string]interface{} + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("getnetworkinfo unmarshall: %v", err) + return false + } + + return response["version"].(float64) >= 230202 +} + +func CreateClaimRawTx(peginTxId string, + peginVout uint, + peginRawTx string, + peginTxoutProof string, + peginClaimScript string, + peginAmount uint64, + liquidAddress string, + liquidAddress2 string, + fee uint64) (string, error) { + + client := ElementsClient() + service := &Elements{client} + wallet := config.Config.ElementsWallet + + // Create the inputs array + inputs := []map[string]interface{}{ + { + "txid": peginTxId, + "vout": peginVout, + "pegin_bitcoin_tx": peginRawTx, + "pegin_txout_proof": peginTxoutProof, + "pegin_claim_script": peginClaimScript, + }, + } + + // Create the outputs array + outputs := []map[string]interface{}{ + { + liquidAddress: toBitcoin(peginAmount - fee - 2000), + }, + { + liquidAddress2: toBitcoin(2000), + }, + { + "fee": toBitcoin(fee), + }, + } + + // Combine inputs and outputs into the parameters array + params := []interface{}{inputs, outputs} + + r, err := service.client.call("createrawtransaction", params, "/wallet/"+wallet) + if err = handleError(err, &r); err != nil { + log.Printf("Failed to create raw transaction: %v", err) + return "", err + } + + var response string + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("CreateClaimRawTx unmarshall: %v", err) + return "", err + } + + return response, nil +} + +func CreateClaimPSBT(peginTxId string, + peginVout uint, + peginRawTx string, + peginTxoutProof string, + peginClaimScript string, + peginAmount uint64, + liquidAddress string, + peginTxId2 string, + peginVout2 uint, + peginRawTx2 string, + peginTxoutProof2 string, + peginClaimScript2 string, + peginAmount2 uint64, + liquidAddress2 string, + fee uint64) (string, error) { + + client := ElementsClient() + service := &Elements{client} + wallet := config.Config.ElementsWallet + + // Create the inputs array + inputs := []map[string]interface{}{ + { + "txid": peginTxId, + "vout": peginVout, + "pegin_bitcoin_tx": peginRawTx, + "pegin_txout_proof": peginTxoutProof, + "pegin_claim_script": peginClaimScript, + }, + { + "txid": peginTxId2, + "vout": peginVout2, + "pegin_bitcoin_tx": peginRawTx2, + "pegin_txout_proof": peginTxoutProof2, + "pegin_claim_script": peginClaimScript2, + }, + } + + // Create the outputs array + outputs := []map[string]interface{}{ + { + liquidAddress: toBitcoin(peginAmount - fee/2), + "blinder_index": 0, + }, + { + liquidAddress2: toBitcoin(peginAmount2 - fee/2), + "blinder_index": 1, + }, + { + "fee": toBitcoin(fee), + }, + } + + // Combine inputs and outputs into the parameters array + params := []interface{}{inputs, outputs} + + r, err := service.client.call("createpsbt", params, "/wallet/"+wallet) + if err = handleError(err, &r); err != nil { + log.Printf("Failed to create raw transaction: %v", err) + return "", err + } + + var response string + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("CreateClaimRawTx unmarshall: %v", err) + return "", err + } + + return response, nil +} + +func BlindRawTx(hexRawTx, wallet string) (string, error) { + + client := ElementsClient() + service := &Elements{client} + + params := []interface{}{hexRawTx, false} + + r, err := service.client.call("blindrawtransaction", params, "/wallet/"+wallet) + if err = handleError(err, &r); err != nil { + log.Printf("Failed to blind raw transaction: %v", err) + return "", err + } + + var response string + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("BlindRawTx unmarshall: %v", err) + return "", err + } + + return response, nil +} + +func ConvertToPsbt(hexRawTx string) (string, error) { + + client := ElementsClient() + service := &Elements{client} + wallet := config.Config.ElementsWallet + + params := []interface{}{hexRawTx} + + r, err := service.client.call("converttopsbt", params, "/wallet/"+wallet) + if err = handleError(err, &r); err != nil { + log.Printf("Failed to convert to psbt: %v", err) + return "", err + } + + var response string + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("ConvertToPsbt unmarshall: %v", err) + return "", err + } + + return response, nil +} + +func SignRawTx(hexRawTx, wallet string) (string, error) { + + client := ElementsClient() + service := &Elements{client} + + params := []interface{}{hexRawTx} + + r, err := service.client.call("signrawtransactionwithwallet", params, "/wallet/"+wallet) + if err = handleError(err, &r); err != nil { + log.Printf("Failed to sign raw transaction: %v", err) + return "", err + } + + var response map[string]interface{} + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("SignRawTx unmarshall: %v", err) + return "", err + } + + return response["hex"].(string), nil +} + +func CombineRawTx(hexRawTx, haxRawTx2 string) (string, error) { + + client := ElementsClient() + service := &Elements{client} + + params := []interface{}{[]string{hexRawTx, haxRawTx2}} + + r, err := service.client.call("combinerawtransaction", params, "") + if err = handleError(err, &r); err != nil { + log.Printf("Failed to combine raw transaction: %v", err) + return "", err + } + + var response map[string]interface{} + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("CombineRawTx unmarshall: %v", err) + return "", err + } + + return response["hex"].(string), nil +} + +func CombinePSBT(base64Tx1, base64Tx2 string) (string, error) { + + client := ElementsClient() + service := &Elements{client} + + params := []interface{}{[]string{base64Tx1, base64Tx2}} + + r, err := service.client.call("combinepsbt", params, "") + if err = handleError(err, &r); err != nil { + log.Printf("Failed to combine PSBT: %v", err) + return "", err + } + + var response map[string]interface{} + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("CombinePSBT unmarshall: %v", err) + return "", err + } + + return response["hex"].(string), nil +} + +func ProcessPSBT(base64psbt, wallet string) (string, error) { + + client := ElementsClient() + service := &Elements{client} + + params := []interface{}{base64psbt} + + r, err := service.client.call("walletprocesspsbt", params, "/wallet/"+wallet) + if err = handleError(err, &r); err != nil { + log.Printf("Failed to process PSBT: %v", err) + return "", err + } + + var response map[string]interface{} + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("ProcessPSBT unmarshall: %v", err) + return "", err + } + + return response["psbt"].(string), nil +} diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 11cfeae..1ce57ce 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -35,7 +35,7 @@ import ( const ( // App version tag - version = "v1.6.8" + version = "v1.7.0" // Swap Out reserves are hardcoded here: // https://github.com/ElementsProject/peerswap/blob/c77a82913d7898d0d3b7c83e4a990abf54bd97e5/peerswaprpc/server.go#L105 @@ -75,6 +75,8 @@ var ( peerNodeId = make(map[uint64]string) // only poll all peers once after peerswap initializes initalPollComplete = false + // identifies if this version of Elements Core supports discounted vSize + hasDiscountedvSize = false ) func main() { @@ -126,6 +128,69 @@ func main() { } defer cleanup() + hasDiscountedvSize = liquid.HasDiscountedvSize() + + // first pegin + peginTxId := "0b387c3b7a8d9fad4d7a1ac2cba4958451c03d6c4fe63dfbe10cdb86d666cdd7" + peginVout := uint(0) + peginRawTx := "0200000000010159c1a062851325f301282b8ae1124c98228ea7b100eb82e907c6c314001a11860100000000fdffffff03a08601000000000017a914c74490c48b105376e697925c314c47afb00f1303872b1400000000000017a914b5e28484d1f1a5d74878f6ef411d555ac170e62887400d03000000000017a9146ec12f7b07420d693b59c8e6c20b17fbe40503ae87024730440220687049a9caf086d6a2205534acde99e549bde1bb922cfb6f7a7f5204a48ccebc02201c0d84dff34c3ce455fc6806015c160fa35eee86d2fddc7ceebdc3eb4d3d18d20121038d432d6aa857671ed9b7c67d62b0d2ae3c930e203024978c92a0de8b84142ce58a910000" + peginTxoutProof := "0000002015fa046cecb94e68d91a1c9410d3a220c34a738121b56f17270000000000000036a1af20a2d5bd8c81ddd4b1444f14b28c9c3d049fb2ae6280b0b1ebf19aca64ca4caa660793601934cfc0800e000000044934a86fa650c138bd770a4ca085d35118f433f02f7660ac8b77d4e99120a241cf9ddc148344fd9ed151ecc9b73697ac6d7ac34a2e1ae588f0b39e5b26e39841cd0c149927b444b7cfbb1c9a14f6f7b1e7e578ba2fb55d3999df736fc087e89fd7cd66d686db0ce1fb3de64f6c3dc0518495a4cbc21a7a4dad9f8d7a3b7c380b01b5" + peginClaimScript := "00142c72a18f520851c0da25c3b9a4a7be1daf65a7a3" + peginAmount := uint64(100_000) + liquidAddress := "el1qq2ssn76875d2624p8fmzlm4u959kasmuss0wl4hxdm6hrcz8syruxgx7esshkshs6rdxrrzru7ujw7ne6h3asd46hj3ruv8xh" + + // second pegin + peginTxId2 := "0b387c3b7a8d9fad4d7a1ac2cba4958451c03d6c4fe63dfbe10cdb86d666cdd7" + peginVout2 := uint(2) + peginRawTx2 := "0200000000010159c1a062851325f301282b8ae1124c98228ea7b100eb82e907c6c314001a11860100000000fdffffff03a08601000000000017a914c74490c48b105376e697925c314c47afb00f1303872b1400000000000017a914b5e28484d1f1a5d74878f6ef411d555ac170e62887400d03000000000017a9146ec12f7b07420d693b59c8e6c20b17fbe40503ae87024730440220687049a9caf086d6a2205534acde99e549bde1bb922cfb6f7a7f5204a48ccebc02201c0d84dff34c3ce455fc6806015c160fa35eee86d2fddc7ceebdc3eb4d3d18d20121038d432d6aa857671ed9b7c67d62b0d2ae3c930e203024978c92a0de8b84142ce58a910000" + peginTxoutProof2 := "0000002015fa046cecb94e68d91a1c9410d3a220c34a738121b56f17270000000000000036a1af20a2d5bd8c81ddd4b1444f14b28c9c3d049fb2ae6280b0b1ebf19aca64ca4caa660793601934cfc0800e000000044934a86fa650c138bd770a4ca085d35118f433f02f7660ac8b77d4e99120a241cf9ddc148344fd9ed151ecc9b73697ac6d7ac34a2e1ae588f0b39e5b26e39841cd0c149927b444b7cfbb1c9a14f6f7b1e7e578ba2fb55d3999df736fc087e89fd7cd66d686db0ce1fb3de64f6c3dc0518495a4cbc21a7a4dad9f8d7a3b7c380b01b5" + peginClaimScript2 := "0014e6f7021314806b914a45cce95680b1377f0b7003" + peginAmount2 := uint64(200_000) + liquidAddress2 := "el1qqfun028g4f2nen6a5zj8t20jrsg258k023azkp075rx529g95nf2vysemv6qhkzlntx4gw3tn9ptc0ynr86nqvfaxkar73zzw" + + fee := uint64(100) + + psbt, err := liquid.CreateClaimPSBT(peginTxId, + peginVout, + peginRawTx, + peginTxoutProof, + peginClaimScript, + peginAmount, + liquidAddress, + peginTxId2, + peginVout2, + peginRawTx2, + peginTxoutProof2, + peginClaimScript2, + peginAmount2, + liquidAddress2, + fee) + + if err != nil { + log.Fatalln(err) + } + + log.Println(psbt) + + signed1, err := liquid.ProcessPSBT(psbt, config.Config.ElementsWallet) + if err != nil { + log.Fatalln(err) + } + + log.Println(signed1) + + signed2, err := liquid.ProcessPSBT(signed1, "swaplnd2") + if err != nil { + log.Fatalln(err) + } + + //combined, err := liquid.CombinePSBT(signed1, signed2) + //if err != nil { + // log.Fatalln(err) + //} + + log.Println(signed2) + // Load persisted data from database ln.LoadDB() db.Load("Peers", "NodeId", &peerNodeId) From 4b2acc64c6e84b352039184455f8707216b04440 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Thu, 1 Aug 2024 15:06:42 +0200 Subject: [PATCH 02/98] Cannot combine PSETs --- cmd/psweb/liquid/elements.go | 187 ++++++----------------------------- cmd/psweb/main.go | 33 ++++--- 2 files changed, 50 insertions(+), 170 deletions(-) diff --git a/cmd/psweb/liquid/elements.go b/cmd/psweb/liquid/elements.go index d6f065e..cb8dcbd 100644 --- a/cmd/psweb/liquid/elements.go +++ b/cmd/psweb/liquid/elements.go @@ -441,64 +441,6 @@ func HasDiscountedvSize() bool { return response["version"].(float64) >= 230202 } -func CreateClaimRawTx(peginTxId string, - peginVout uint, - peginRawTx string, - peginTxoutProof string, - peginClaimScript string, - peginAmount uint64, - liquidAddress string, - liquidAddress2 string, - fee uint64) (string, error) { - - client := ElementsClient() - service := &Elements{client} - wallet := config.Config.ElementsWallet - - // Create the inputs array - inputs := []map[string]interface{}{ - { - "txid": peginTxId, - "vout": peginVout, - "pegin_bitcoin_tx": peginRawTx, - "pegin_txout_proof": peginTxoutProof, - "pegin_claim_script": peginClaimScript, - }, - } - - // Create the outputs array - outputs := []map[string]interface{}{ - { - liquidAddress: toBitcoin(peginAmount - fee - 2000), - }, - { - liquidAddress2: toBitcoin(2000), - }, - { - "fee": toBitcoin(fee), - }, - } - - // Combine inputs and outputs into the parameters array - params := []interface{}{inputs, outputs} - - r, err := service.client.call("createrawtransaction", params, "/wallet/"+wallet) - if err = handleError(err, &r); err != nil { - log.Printf("Failed to create raw transaction: %v", err) - return "", err - } - - var response string - - err = json.Unmarshal([]byte(r.Result), &response) - if err != nil { - log.Printf("CreateClaimRawTx unmarshall: %v", err) - return "", err - } - - return response, nil -} - func CreateClaimPSBT(peginTxId string, peginVout uint, peginRawTx string, @@ -517,7 +459,6 @@ func CreateClaimPSBT(peginTxId string, client := ElementsClient() service := &Elements{client} - wallet := config.Config.ElementsWallet // Create the inputs array inputs := []map[string]interface{}{ @@ -540,22 +481,25 @@ func CreateClaimPSBT(peginTxId string, // Create the outputs array outputs := []map[string]interface{}{ { - liquidAddress: toBitcoin(peginAmount - fee/2), + liquidAddress: toBitcoin(peginAmount), "blinder_index": 0, }, { - liquidAddress2: toBitcoin(peginAmount2 - fee/2), + liquidAddress2: toBitcoin(peginAmount2), "blinder_index": 1, }, - { + } + + if fee > 0 { + outputs = append(outputs, map[string]interface{}{ "fee": toBitcoin(fee), - }, + }) } // Combine inputs and outputs into the parameters array params := []interface{}{inputs, outputs} - r, err := service.client.call("createpsbt", params, "/wallet/"+wallet) + r, err := service.client.call("createpsbt", params, "") if err = handleError(err, &r); err != nil { log.Printf("Failed to create raw transaction: %v", err) return "", err @@ -572,114 +516,41 @@ func CreateClaimPSBT(peginTxId string, return response, nil } -func BlindRawTx(hexRawTx, wallet string) (string, error) { - - client := ElementsClient() - service := &Elements{client} - - params := []interface{}{hexRawTx, false} - - r, err := service.client.call("blindrawtransaction", params, "/wallet/"+wallet) - if err = handleError(err, &r); err != nil { - log.Printf("Failed to blind raw transaction: %v", err) - return "", err - } - - var response string - - err = json.Unmarshal([]byte(r.Result), &response) - if err != nil { - log.Printf("BlindRawTx unmarshall: %v", err) - return "", err - } - - return response, nil -} - -func ConvertToPsbt(hexRawTx string) (string, error) { - - client := ElementsClient() - service := &Elements{client} - wallet := config.Config.ElementsWallet - - params := []interface{}{hexRawTx} - - r, err := service.client.call("converttopsbt", params, "/wallet/"+wallet) - if err = handleError(err, &r); err != nil { - log.Printf("Failed to convert to psbt: %v", err) - return "", err - } - - var response string - - err = json.Unmarshal([]byte(r.Result), &response) - if err != nil { - log.Printf("ConvertToPsbt unmarshall: %v", err) - return "", err - } - - return response, nil -} - -func SignRawTx(hexRawTx, wallet string) (string, error) { +func ProcessPSBT(base64psbt, wallet string) (string, bool, error) { client := ElementsClient() service := &Elements{client} - params := []interface{}{hexRawTx} - - r, err := service.client.call("signrawtransactionwithwallet", params, "/wallet/"+wallet) - if err = handleError(err, &r); err != nil { - log.Printf("Failed to sign raw transaction: %v", err) - return "", err - } - - var response map[string]interface{} - - err = json.Unmarshal([]byte(r.Result), &response) - if err != nil { - log.Printf("SignRawTx unmarshall: %v", err) - return "", err - } - - return response["hex"].(string), nil -} - -func CombineRawTx(hexRawTx, haxRawTx2 string) (string, error) { - - client := ElementsClient() - service := &Elements{client} - - params := []interface{}{[]string{hexRawTx, haxRawTx2}} + params := []interface{}{base64psbt} - r, err := service.client.call("combinerawtransaction", params, "") + r, err := service.client.call("walletprocesspsbt", params, "/wallet/"+wallet) if err = handleError(err, &r); err != nil { - log.Printf("Failed to combine raw transaction: %v", err) - return "", err + log.Printf("Failed to process PSBT: %v", err) + return "", false, err } var response map[string]interface{} err = json.Unmarshal([]byte(r.Result), &response) if err != nil { - log.Printf("CombineRawTx unmarshall: %v", err) - return "", err + log.Printf("ProcessPSBT unmarshall: %v", err) + return "", false, err } - return response["hex"].(string), nil + return response["psbt"].(string), response["complete"].(bool), nil } -func CombinePSBT(base64Tx1, base64Tx2 string) (string, error) { +func CombinePSBT(psbt []string) (string, bool, error) { client := ElementsClient() service := &Elements{client} - params := []interface{}{[]string{base64Tx1, base64Tx2}} + params := []interface{}{psbt} r, err := service.client.call("combinepsbt", params, "") if err = handleError(err, &r); err != nil { log.Printf("Failed to combine PSBT: %v", err) - return "", err + return "", false, err } var response map[string]interface{} @@ -687,32 +558,32 @@ func CombinePSBT(base64Tx1, base64Tx2 string) (string, error) { err = json.Unmarshal([]byte(r.Result), &response) if err != nil { log.Printf("CombinePSBT unmarshall: %v", err) - return "", err + return "", false, err } - return response["hex"].(string), nil + return response["hex"].(string), response["complete"].(bool), nil } -func ProcessPSBT(base64psbt, wallet string) (string, error) { +func FinalizePSBT(psbt string) (string, bool, error) { client := ElementsClient() service := &Elements{client} - params := []interface{}{base64psbt} + params := []interface{}{psbt} - r, err := service.client.call("walletprocesspsbt", params, "/wallet/"+wallet) + r, err := service.client.call("finalizepsbt", params, "") if err = handleError(err, &r); err != nil { - log.Printf("Failed to process PSBT: %v", err) - return "", err + log.Printf("Failed to finalize PSBT: %v", err) + return "", false, err } var response map[string]interface{} err = json.Unmarshal([]byte(r.Result), &response) if err != nil { - log.Printf("ProcessPSBT unmarshall: %v", err) - return "", err + log.Printf("FinalizePSBT unmarshall: %v", err) + return "", false, err } - return response["psbt"].(string), nil + return response["hex"].(string), response["complete"].(bool), nil } diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 1ce57ce..9463f86 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -155,41 +155,50 @@ func main() { peginRawTx, peginTxoutProof, peginClaimScript, - peginAmount, + peginAmount-fee, liquidAddress, peginTxId2, peginVout2, peginRawTx2, peginTxoutProof2, peginClaimScript2, - peginAmount2, + peginAmount2-fee, liquidAddress2, - fee) + fee*2) + if err != nil { + log.Fatalln(err) + } + + signed1, complete, err := liquid.ProcessPSBT(psbt, config.Config.ElementsWallet) + if err != nil { + log.Fatalln(err) + } + + log.Println(signed1, complete) + signed2, complete, err := liquid.ProcessPSBT(psbt, "swaplnd2") if err != nil { log.Fatalln(err) } - log.Println(psbt) + log.Println(signed2, complete) - signed1, err := liquid.ProcessPSBT(psbt, config.Config.ElementsWallet) + combined, complete, err := liquid.CombinePSBT([]string{signed1, signed2}) if err != nil { log.Fatalln(err) } - log.Println(signed1) + log.Println(combined, complete) - signed2, err := liquid.ProcessPSBT(signed1, "swaplnd2") + hexTx, complete, err := liquid.FinalizePSBT(combined) if err != nil { log.Fatalln(err) } - //combined, err := liquid.CombinePSBT(signed1, signed2) - //if err != nil { - // log.Fatalln(err) - //} + log.Println(hexTx, complete) - log.Println(signed2) + // Exit the program gracefully + os.Exit(0) // Load persisted data from database ln.LoadDB() From 5f7338e45c1c2105b371d8516d1aebe47d18df74 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Thu, 1 Aug 2024 17:15:25 +0200 Subject: [PATCH 03/98] does not work --- cmd/psweb/liquid/elements.go | 25 +++++++++++-------------- cmd/psweb/main.go | 9 ++++----- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/cmd/psweb/liquid/elements.go b/cmd/psweb/liquid/elements.go index cb8dcbd..25ac59c 100644 --- a/cmd/psweb/liquid/elements.go +++ b/cmd/psweb/liquid/elements.go @@ -482,18 +482,15 @@ func CreateClaimPSBT(peginTxId string, outputs := []map[string]interface{}{ { liquidAddress: toBitcoin(peginAmount), - "blinder_index": 0, + "blinder_index": 1, }, { liquidAddress2: toBitcoin(peginAmount2), - "blinder_index": 1, + "blinder_index": 0, }, - } - - if fee > 0 { - outputs = append(outputs, map[string]interface{}{ + { "fee": toBitcoin(fee), - }) + }, } // Combine inputs and outputs into the parameters array @@ -540,7 +537,7 @@ func ProcessPSBT(base64psbt, wallet string) (string, bool, error) { return response["psbt"].(string), response["complete"].(bool), nil } -func CombinePSBT(psbt []string) (string, bool, error) { +func CombinePSBT(psbt []string) (string, error) { client := ElementsClient() service := &Elements{client} @@ -550,28 +547,28 @@ func CombinePSBT(psbt []string) (string, bool, error) { r, err := service.client.call("combinepsbt", params, "") if err = handleError(err, &r); err != nil { log.Printf("Failed to combine PSBT: %v", err) - return "", false, err + return "", err } - var response map[string]interface{} + var response string err = json.Unmarshal([]byte(r.Result), &response) if err != nil { log.Printf("CombinePSBT unmarshall: %v", err) - return "", false, err + return "", err } - return response["hex"].(string), response["complete"].(bool), nil + return response, nil } -func FinalizePSBT(psbt string) (string, bool, error) { +func FinalizePSBT(psbt, wallet string) (string, bool, error) { client := ElementsClient() service := &Elements{client} params := []interface{}{psbt} - r, err := service.client.call("finalizepsbt", params, "") + r, err := service.client.call("finalizepsbt", params, "/wallet/"+wallet) if err = handleError(err, &r); err != nil { log.Printf("Failed to finalize PSBT: %v", err) return "", false, err diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 9463f86..82a1410 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -147,7 +147,6 @@ func main() { peginClaimScript2 := "0014e6f7021314806b914a45cce95680b1377f0b7003" peginAmount2 := uint64(200_000) liquidAddress2 := "el1qqfun028g4f2nen6a5zj8t20jrsg258k023azkp075rx529g95nf2vysemv6qhkzlntx4gw3tn9ptc0ynr86nqvfaxkar73zzw" - fee := uint64(100) psbt, err := liquid.CreateClaimPSBT(peginTxId, @@ -169,7 +168,7 @@ func main() { log.Fatalln(err) } - signed1, complete, err := liquid.ProcessPSBT(psbt, config.Config.ElementsWallet) + signed1, complete, err := liquid.ProcessPSBT(psbt, "swaplnd") if err != nil { log.Fatalln(err) } @@ -183,14 +182,14 @@ func main() { log.Println(signed2, complete) - combined, complete, err := liquid.CombinePSBT([]string{signed1, signed2}) + combined, err := liquid.CombinePSBT([]string{signed1, signed2}) if err != nil { log.Fatalln(err) } - log.Println(combined, complete) + log.Println(combined) - hexTx, complete, err := liquid.FinalizePSBT(combined) + hexTx, complete, err := liquid.FinalizePSBT(combined, "swaplnd2") if err != nil { log.Fatalln(err) } From f479d6ec7074c5a61c50354696d56a05999190e1 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Thu, 1 Aug 2024 22:04:29 +0200 Subject: [PATCH 04/98] works! --- cmd/psweb/liquid/elements.go | 14 +++++++++----- cmd/psweb/main.go | 23 +++++++++++++++-------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/cmd/psweb/liquid/elements.go b/cmd/psweb/liquid/elements.go index 25ac59c..5a7149c 100644 --- a/cmd/psweb/liquid/elements.go +++ b/cmd/psweb/liquid/elements.go @@ -482,11 +482,11 @@ func CreateClaimPSBT(peginTxId string, outputs := []map[string]interface{}{ { liquidAddress: toBitcoin(peginAmount), - "blinder_index": 1, + "blinder_index": 0, }, { liquidAddress2: toBitcoin(peginAmount2), - "blinder_index": 0, + "blinder_index": 1, }, { "fee": toBitcoin(fee), @@ -561,14 +561,14 @@ func CombinePSBT(psbt []string) (string, error) { return response, nil } -func FinalizePSBT(psbt, wallet string) (string, bool, error) { +func FinalizePSBT(psbt string) (string, bool, error) { client := ElementsClient() service := &Elements{client} params := []interface{}{psbt} - r, err := service.client.call("finalizepsbt", params, "/wallet/"+wallet) + r, err := service.client.call("finalizepsbt", params, "") if err = handleError(err, &r); err != nil { log.Printf("Failed to finalize PSBT: %v", err) return "", false, err @@ -582,5 +582,9 @@ func FinalizePSBT(psbt, wallet string) (string, bool, error) { return "", false, err } - return response["hex"].(string), response["complete"].(bool), nil + if response["complete"].(bool) { + return response["hex"].(string), true, nil + } + + return response["psbt"].(string), false, nil } diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 82a1410..348889a 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -147,7 +147,7 @@ func main() { peginClaimScript2 := "0014e6f7021314806b914a45cce95680b1377f0b7003" peginAmount2 := uint64(200_000) liquidAddress2 := "el1qqfun028g4f2nen6a5zj8t20jrsg258k023azkp075rx529g95nf2vysemv6qhkzlntx4gw3tn9ptc0ynr86nqvfaxkar73zzw" - fee := uint64(100) + fee := uint64(33) // per pegin! psbt, err := liquid.CreateClaimPSBT(peginTxId, peginVout, @@ -168,28 +168,35 @@ func main() { log.Fatalln(err) } - signed1, complete, err := liquid.ProcessPSBT(psbt, "swaplnd") + blinded1, complete, err := liquid.ProcessPSBT(psbt, "swaplnd") if err != nil { log.Fatalln(err) } - log.Println(signed1, complete) + log.Println(blinded1, complete) - signed2, complete, err := liquid.ProcessPSBT(psbt, "swaplnd2") + blinded2, complete, err := liquid.ProcessPSBT(blinded1, "swaplnd2") if err != nil { log.Fatalln(err) } - log.Println(signed2, complete) + log.Println(blinded2, complete) - combined, err := liquid.CombinePSBT([]string{signed1, signed2}) + signed1, complete, err := liquid.ProcessPSBT(blinded2, "swaplnd") if err != nil { log.Fatalln(err) } - log.Println(combined) + log.Println(signed1, complete) + + signed2, complete, err := liquid.ProcessPSBT(signed1, "swaplnd2") + if err != nil { + log.Fatalln(err) + } + + log.Println(signed2, complete) - hexTx, complete, err := liquid.FinalizePSBT(combined, "swaplnd2") + hexTx, complete, err := liquid.FinalizePSBT(signed2) if err != nil { log.Fatalln(err) } From 4232cb1ce749238a792234cf33b6bf36f641fd73 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Fri, 2 Aug 2024 14:44:33 +0200 Subject: [PATCH 05/98] add messages --- cmd/psweb/cj/claimjoin.go | 237 ++++++++++++++++++++++++++++++++++++++ cmd/psweb/ln/common.go | 5 + cmd/psweb/ln/lnd.go | 17 ++- cmd/psweb/main.go | 76 ------------ go.mod | 4 +- go.sum | 4 +- 6 files changed, 261 insertions(+), 82 deletions(-) create mode 100644 cmd/psweb/cj/claimjoin.go diff --git a/cmd/psweb/cj/claimjoin.go b/cmd/psweb/cj/claimjoin.go new file mode 100644 index 0000000..e68eb97 --- /dev/null +++ b/cmd/psweb/cj/claimjoin.go @@ -0,0 +1,237 @@ +package cj + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/pem" + "fmt" + "log" + + "peerswap-web/cmd/psweb/config" + "peerswap-web/cmd/psweb/db" + "peerswap-web/cmd/psweb/ln" + "peerswap-web/cmd/psweb/ps" +) + +var ( + myPrivateKey *rsa.PrivateKey + myPublicPEM string + // maps learned public keys to node Id + pemToNodeId map[string]string + // public key of the sender of pegin_started broadcast + PeginHandler string +) + +/* + // first pegin + peginTxId := "0b387c3b7a8d9fad4d7a1ac2cba4958451c03d6c4fe63dfbe10cdb86d666cdd7" + peginVout := uint(0) + peginRawTx := "0200000000010159c1a062851325f301282b8ae1124c98228ea7b100eb82e907c6c314001a11860100000000fdffffff03a08601000000000017a914c74490c48b105376e697925c314c47afb00f1303872b1400000000000017a914b5e28484d1f1a5d74878f6ef411d555ac170e62887400d03000000000017a9146ec12f7b07420d693b59c8e6c20b17fbe40503ae87024730440220687049a9caf086d6a2205534acde99e549bde1bb922cfb6f7a7f5204a48ccebc02201c0d84dff34c3ce455fc6806015c160fa35eee86d2fddc7ceebdc3eb4d3d18d20121038d432d6aa857671ed9b7c67d62b0d2ae3c930e203024978c92a0de8b84142ce58a910000" + peginTxoutProof := "0000002015fa046cecb94e68d91a1c9410d3a220c34a738121b56f17270000000000000036a1af20a2d5bd8c81ddd4b1444f14b28c9c3d049fb2ae6280b0b1ebf19aca64ca4caa660793601934cfc0800e000000044934a86fa650c138bd770a4ca085d35118f433f02f7660ac8b77d4e99120a241cf9ddc148344fd9ed151ecc9b73697ac6d7ac34a2e1ae588f0b39e5b26e39841cd0c149927b444b7cfbb1c9a14f6f7b1e7e578ba2fb55d3999df736fc087e89fd7cd66d686db0ce1fb3de64f6c3dc0518495a4cbc21a7a4dad9f8d7a3b7c380b01b5" + peginClaimScript := "00142c72a18f520851c0da25c3b9a4a7be1daf65a7a3" + peginAmount := uint64(100_000) + liquidAddress := "el1qq2ssn76875d2624p8fmzlm4u959kasmuss0wl4hxdm6hrcz8syruxgx7esshkshs6rdxrrzru7ujw7ne6h3asd46hj3ruv8xh" + + // second pegin + peginTxId2 := "0b387c3b7a8d9fad4d7a1ac2cba4958451c03d6c4fe63dfbe10cdb86d666cdd7" + peginVout2 := uint(2) + peginRawTx2 := "0200000000010159c1a062851325f301282b8ae1124c98228ea7b100eb82e907c6c314001a11860100000000fdffffff03a08601000000000017a914c74490c48b105376e697925c314c47afb00f1303872b1400000000000017a914b5e28484d1f1a5d74878f6ef411d555ac170e62887400d03000000000017a9146ec12f7b07420d693b59c8e6c20b17fbe40503ae87024730440220687049a9caf086d6a2205534acde99e549bde1bb922cfb6f7a7f5204a48ccebc02201c0d84dff34c3ce455fc6806015c160fa35eee86d2fddc7ceebdc3eb4d3d18d20121038d432d6aa857671ed9b7c67d62b0d2ae3c930e203024978c92a0de8b84142ce58a910000" + peginTxoutProof2 := "0000002015fa046cecb94e68d91a1c9410d3a220c34a738121b56f17270000000000000036a1af20a2d5bd8c81ddd4b1444f14b28c9c3d049fb2ae6280b0b1ebf19aca64ca4caa660793601934cfc0800e000000044934a86fa650c138bd770a4ca085d35118f433f02f7660ac8b77d4e99120a241cf9ddc148344fd9ed151ecc9b73697ac6d7ac34a2e1ae588f0b39e5b26e39841cd0c149927b444b7cfbb1c9a14f6f7b1e7e578ba2fb55d3999df736fc087e89fd7cd66d686db0ce1fb3de64f6c3dc0518495a4cbc21a7a4dad9f8d7a3b7c380b01b5" + peginClaimScript2 := "0014e6f7021314806b914a45cce95680b1377f0b7003" + peginAmount2 := uint64(200_000) + liquidAddress2 := "el1qqfun028g4f2nen6a5zj8t20jrsg258k023azkp075rx529g95nf2vysemv6qhkzlntx4gw3tn9ptc0ynr86nqvfaxkar73zzw" + fee := uint64(33) // per pegin! + + psbt, err := liquid.CreateClaimPSBT(peginTxId, + peginVout, + peginRawTx, + peginTxoutProof, + peginClaimScript, + peginAmount-fee, + liquidAddress, + peginTxId2, + peginVout2, + peginRawTx2, + peginTxoutProof2, + peginClaimScript2, + peginAmount2-fee, + liquidAddress2, + fee*2) + if err != nil { + log.Fatalln(err) + } + + blinded1, complete, err := liquid.ProcessPSBT(psbt, "swaplnd") + if err != nil { + log.Fatalln(err) + } + + blinded2, complete, err := liquid.ProcessPSBT(blinded1, "swaplnd2") + if err != nil { + log.Fatalln(err) + } + + signed1, complete, err := liquid.ProcessPSBT(blinded2, "swaplnd") + if err != nil { + log.Fatalln(err) + } + + signed2, complete, err := liquid.ProcessPSBT(signed1, "swaplnd2") + if err != nil { + log.Fatalln(err) + } + + hexTx, complete, err := liquid.FinalizePSBT(signed2) + if err != nil { + log.Fatalln(err) + } +*/ + +// Forward the announcement to all direct peers, unless the source PEM is known already +func BroadcastMessage(fromNodeId string, message *ln.Message) error { + + if pemToNodeId[message.Sender] != "" { + // has been allready received from some nearer node + return nil + } + + client, cleanup, err := ps.GetClient(config.Config.RpcHost) + if err != nil { + return err + } + defer cleanup() + + res, err := ps.ListPeers(client) + if err != nil { + return err + } + + cl, clean, er := ln.GetClient() + if er != nil { + return err + } + defer clean() + + for _, peer := range res.GetPeers() { + // don't send back to where it came from + if peer.NodeId != fromNodeId { + ln.SendCustomMessage(cl, peer.NodeId, message) + } + } + + if message.Asset == "pegin_started" { + // where to forward claimjoin request + PeginHandler = message.Sender + // store for relaying further encrypted messages + pemToNodeId[message.Sender] = fromNodeId + } else if message.Asset == "pegin_ended" { + PeginHandler = "" + // delete key data from the map + delete(pemToNodeId, message.Sender) + } + + // persist to db + db.Save("ClaimJoin", "PemToNodeId", &pemToNodeId) + db.Save("ClaimJoin", "PeginHandler", &PeginHandler) + + return nil +} + +// Encrypt and send message to an anonymous peer identified only by his public key in PEM format +// New keys are generated at the start of each pegin session +// Peers track sources of encrypted messages to forward back the replies +func SendEncryptedMessage(destinationPEM string, payload []byte) error { + + destinationNodeId := pemToNodeId[destinationPEM] + if destinationNodeId == "" { + return fmt.Errorf("destination PEM has no matching NodeId") + } + + // Deserialize the received PEM string back to a public key + deserializedPublicKey, err := pemToPublicKey(destinationPEM) + if err != nil { + log.Println("Error deserializing public key:", err) + return err + } + + // Encrypt the message using the deserialized receiver's public key + ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, deserializedPublicKey, payload, nil) + if err != nil { + log.Println("Error encrypting message:", err) + return err + } + + cl, clean, er := ln.GetClient() + if er != nil { + return er + } + defer clean() + + return ln.SendCustomMessage(cl, destinationNodeId, &ln.Message{ + Version: ln.MessageVersion, + Memo: "relay", + Sender: myPublicPEM, + Destination: destinationPEM, + Payload: ciphertext, + }) +} + +// generates new message keys +func GenerateKeys() error { + // Generate RSA key pair + var err error + + myPrivateKey, err = rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + log.Println("Error generating my private key:", err) + return err + } + + myPublicPEM, err = publicKeyToPEM(&myPrivateKey.PublicKey) + if err != nil { + log.Println("Error obtaining my public PEM:", err) + return err + } + + // persist to db + db.Save("ClaimJoin", "PrivateKey", &myPrivateKey) + + return nil +} + +// Convert a public key to PEM format +func publicKeyToPEM(pub *rsa.PublicKey) (string, error) { + pubASN1, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return "", err + } + + pubPEM := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: pubASN1, + }) + + return string(pubPEM), nil +} + +// Convert a PEM-formatted string to a public key +func pemToPublicKey(pubPEM string) (*rsa.PublicKey, error) { + block, _ := pem.Decode([]byte(pubPEM)) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + + switch pub := pub.(type) { + case *rsa.PublicKey: + return pub, nil + default: + return nil, fmt.Errorf("not an RSA public key") + } +} diff --git a/cmd/psweb/ln/common.go b/cmd/psweb/ln/common.go index 25d4ae0..7b5f40e 100644 --- a/cmd/psweb/ln/common.go +++ b/cmd/psweb/ln/common.go @@ -146,10 +146,15 @@ type DataPoint struct { // sent/received as json type Message struct { + // cleartext announcements Version int `json:"version"` Memo string `json:"memo"` Asset string `json:"asset"` Amount uint64 `json:"amount"` + // for encrypted communications via peer relay + Sender string `json:"sender"` + Destination string `json:"destination"` + Payload []byte `json:"payload"` } type BalanceInfo struct { diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 8dcca72..fad839b 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -20,6 +20,7 @@ import ( "time" "peerswap-web/cmd/psweb/bitcoin" + "peerswap-web/cmd/psweb/cj" "peerswap-web/cmd/psweb/config" "peerswap-web/cmd/psweb/db" @@ -1181,14 +1182,22 @@ func subscribeMessages(ctx context.Context, client lnrpc.LightningClient) error var msg Message err := json.Unmarshal(data.Data, &msg) if err != nil { + log.Println("Received an incorrectly formed message") continue } if msg.Version != MessageVersion { + log.Println("Received a message with wrong version number") continue } nodeId := hex.EncodeToString(data.Peer) + // received broadcast of begin status + // msg.Asset = "pegin_started" or "pegin_ended" + if msg.Memo == "broadcast" { + cj.BroadcastMessage(nodeId, &msg) + } + // received request for information if msg.Memo == "poll" { if AdvertiseLiquidBalance { @@ -1528,13 +1537,16 @@ func SendKeysendMessage(destPubkey string, amountSats int64, message string) err func ListPeers(client lnrpc.LightningClient, peerId string, excludeIds *[]string) (*peerswaprpc.ListPeersResponse, error) { ctx := context.Background() - res, err := client.ListPeers(ctx, &lnrpc.ListPeersRequest{}) + res, err := client.ListPeers(ctx, &lnrpc.ListPeersRequest{ + LatestError: true, + }) if err != nil { return nil, err } res2, err := client.ListChannels(ctx, &lnrpc.ListChannelsRequest{ - // PublicOnly: true, + ActiveOnly: false, + PublicOnly: false, }) if err != nil { return nil, err @@ -1570,6 +1582,7 @@ func ListPeers(client lnrpc.LightningClient, peerId string, excludeIds *[]string peer.AsSender = &peerswaprpc.SwapStats{} peer.AsReceiver = &peerswaprpc.SwapStats{} + // skip peers with no channels with us if len(peer.Channels) > 0 { peers = append(peers, &peer) } diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 348889a..4f31e99 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -130,82 +130,6 @@ func main() { hasDiscountedvSize = liquid.HasDiscountedvSize() - // first pegin - peginTxId := "0b387c3b7a8d9fad4d7a1ac2cba4958451c03d6c4fe63dfbe10cdb86d666cdd7" - peginVout := uint(0) - peginRawTx := "0200000000010159c1a062851325f301282b8ae1124c98228ea7b100eb82e907c6c314001a11860100000000fdffffff03a08601000000000017a914c74490c48b105376e697925c314c47afb00f1303872b1400000000000017a914b5e28484d1f1a5d74878f6ef411d555ac170e62887400d03000000000017a9146ec12f7b07420d693b59c8e6c20b17fbe40503ae87024730440220687049a9caf086d6a2205534acde99e549bde1bb922cfb6f7a7f5204a48ccebc02201c0d84dff34c3ce455fc6806015c160fa35eee86d2fddc7ceebdc3eb4d3d18d20121038d432d6aa857671ed9b7c67d62b0d2ae3c930e203024978c92a0de8b84142ce58a910000" - peginTxoutProof := "0000002015fa046cecb94e68d91a1c9410d3a220c34a738121b56f17270000000000000036a1af20a2d5bd8c81ddd4b1444f14b28c9c3d049fb2ae6280b0b1ebf19aca64ca4caa660793601934cfc0800e000000044934a86fa650c138bd770a4ca085d35118f433f02f7660ac8b77d4e99120a241cf9ddc148344fd9ed151ecc9b73697ac6d7ac34a2e1ae588f0b39e5b26e39841cd0c149927b444b7cfbb1c9a14f6f7b1e7e578ba2fb55d3999df736fc087e89fd7cd66d686db0ce1fb3de64f6c3dc0518495a4cbc21a7a4dad9f8d7a3b7c380b01b5" - peginClaimScript := "00142c72a18f520851c0da25c3b9a4a7be1daf65a7a3" - peginAmount := uint64(100_000) - liquidAddress := "el1qq2ssn76875d2624p8fmzlm4u959kasmuss0wl4hxdm6hrcz8syruxgx7esshkshs6rdxrrzru7ujw7ne6h3asd46hj3ruv8xh" - - // second pegin - peginTxId2 := "0b387c3b7a8d9fad4d7a1ac2cba4958451c03d6c4fe63dfbe10cdb86d666cdd7" - peginVout2 := uint(2) - peginRawTx2 := "0200000000010159c1a062851325f301282b8ae1124c98228ea7b100eb82e907c6c314001a11860100000000fdffffff03a08601000000000017a914c74490c48b105376e697925c314c47afb00f1303872b1400000000000017a914b5e28484d1f1a5d74878f6ef411d555ac170e62887400d03000000000017a9146ec12f7b07420d693b59c8e6c20b17fbe40503ae87024730440220687049a9caf086d6a2205534acde99e549bde1bb922cfb6f7a7f5204a48ccebc02201c0d84dff34c3ce455fc6806015c160fa35eee86d2fddc7ceebdc3eb4d3d18d20121038d432d6aa857671ed9b7c67d62b0d2ae3c930e203024978c92a0de8b84142ce58a910000" - peginTxoutProof2 := "0000002015fa046cecb94e68d91a1c9410d3a220c34a738121b56f17270000000000000036a1af20a2d5bd8c81ddd4b1444f14b28c9c3d049fb2ae6280b0b1ebf19aca64ca4caa660793601934cfc0800e000000044934a86fa650c138bd770a4ca085d35118f433f02f7660ac8b77d4e99120a241cf9ddc148344fd9ed151ecc9b73697ac6d7ac34a2e1ae588f0b39e5b26e39841cd0c149927b444b7cfbb1c9a14f6f7b1e7e578ba2fb55d3999df736fc087e89fd7cd66d686db0ce1fb3de64f6c3dc0518495a4cbc21a7a4dad9f8d7a3b7c380b01b5" - peginClaimScript2 := "0014e6f7021314806b914a45cce95680b1377f0b7003" - peginAmount2 := uint64(200_000) - liquidAddress2 := "el1qqfun028g4f2nen6a5zj8t20jrsg258k023azkp075rx529g95nf2vysemv6qhkzlntx4gw3tn9ptc0ynr86nqvfaxkar73zzw" - fee := uint64(33) // per pegin! - - psbt, err := liquid.CreateClaimPSBT(peginTxId, - peginVout, - peginRawTx, - peginTxoutProof, - peginClaimScript, - peginAmount-fee, - liquidAddress, - peginTxId2, - peginVout2, - peginRawTx2, - peginTxoutProof2, - peginClaimScript2, - peginAmount2-fee, - liquidAddress2, - fee*2) - if err != nil { - log.Fatalln(err) - } - - blinded1, complete, err := liquid.ProcessPSBT(psbt, "swaplnd") - if err != nil { - log.Fatalln(err) - } - - log.Println(blinded1, complete) - - blinded2, complete, err := liquid.ProcessPSBT(blinded1, "swaplnd2") - if err != nil { - log.Fatalln(err) - } - - log.Println(blinded2, complete) - - signed1, complete, err := liquid.ProcessPSBT(blinded2, "swaplnd") - if err != nil { - log.Fatalln(err) - } - - log.Println(signed1, complete) - - signed2, complete, err := liquid.ProcessPSBT(signed1, "swaplnd2") - if err != nil { - log.Fatalln(err) - } - - log.Println(signed2, complete) - - hexTx, complete, err := liquid.FinalizePSBT(signed2) - if err != nil { - log.Fatalln(err) - } - - log.Println(hexTx, complete) - - // Exit the program gracefully - os.Exit(0) - // Load persisted data from database ln.LoadDB() db.Load("Peers", "NodeId", &peerNodeId) diff --git a/go.mod b/go.mod index 6f58a7d..6bddf96 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,9 @@ require ( github.com/elementsproject/peerswap v0.2.98-0.20240408181051-0f0d34c6c506 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/gorilla/mux v1.8.1 + github.com/gorilla/sessions v1.2.2 github.com/lightningnetwork/lnd v0.18.0-beta + go.etcd.io/bbolt v1.3.10 golang.org/x/net v0.24.0 google.golang.org/grpc v1.63.2 gopkg.in/macaroon.v2 v2.1.0 @@ -73,7 +75,6 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/gorilla/sessions v1.2.2 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect @@ -148,7 +149,6 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect - go.etcd.io/bbolt v1.3.10 // indirect go.etcd.io/etcd/api/v3 v3.5.7 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect go.etcd.io/etcd/client/v2 v2.305.7 // indirect diff --git a/go.sum b/go.sum index b3b699f..92a0766 100644 --- a/go.sum +++ b/go.sum @@ -290,6 +290,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -625,8 +627,6 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= -go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= go.etcd.io/etcd/api/v3 v3.5.7 h1:sbcmosSVesNrWOJ58ZQFitHMdncusIifYcrBfwrlJSY= From 1445bbd5e9f73303d7dc0145a5ffb256d59d3141 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Sun, 4 Aug 2024 14:09:29 +0200 Subject: [PATCH 06/98] fix swap-in fee estimate --- .vscode/launch.json | 3 +- CHANGELOG.md | 2 +- cmd/psweb/bitcoin/bitcoin.go | 28 ++++++ cmd/psweb/handlers.go | 57 +++-------- cmd/psweb/{cj => ln}/claimjoin.go | 162 ++++++++++++++++++++++++++---- cmd/psweb/ln/cln.go | 4 - cmd/psweb/ln/common.go | 14 +++ cmd/psweb/ln/lnd.go | 22 ++-- cmd/psweb/main.go | 15 ++- go.mod | 7 +- go.sum | 18 +++- 11 files changed, 244 insertions(+), 88 deletions(-) rename cmd/psweb/{cj => ln}/claimjoin.go (62%) diff --git a/.vscode/launch.json b/.vscode/launch.json index 5278380..eab2a29 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,8 @@ "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - // "args": ["-datadir", "/home/vlad/.peerswap2"] + //"args": ["-datadir", "/home/vlad/.peerswap_t4"] + //"args": ["-datadir", "/home/vlad/.peerswap2"] } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index a7ec302..a851a88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 1.7.0 -- +- Better estimate swap-in fee and maximum swap amount ## 1.6.8 diff --git a/cmd/psweb/bitcoin/bitcoin.go b/cmd/psweb/bitcoin/bitcoin.go index ec748d2..cef6bf7 100644 --- a/cmd/psweb/bitcoin/bitcoin.go +++ b/cmd/psweb/bitcoin/bitcoin.go @@ -207,6 +207,34 @@ func GetRawTransaction(txid string, result *Transaction) (string, error) { return raw, nil } +type FeeInfo struct { + Feerate float64 `json:"feerate"` + Blocks int `json:"blocks"` +} + +// Estimate sat/vB fee rate from bitcoin core +func EstimateSatvB(targetConf uint) float64 { + client := BitcoinClient() + service := &Bitcoin{client} + + params := []interface{}{targetConf} + + r, err := service.client.call("estimatesmartfee", params, "") + if err = handleError(err, &r); err != nil { + return 0 + } + + var feeInfo FeeInfo + + err = json.Unmarshal([]byte(r.Result), &feeInfo) + if err != nil { + log.Printf("GetRawTransaction unmarshall raw: %v", err) + return 0 + } + + return feeInfo.Feerate * 100_000 +} + func GetTxOutProof(txid string) (string, error) { client := BitcoinClient() service := &Bitcoin{client} diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index be9545d..cf7661d 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -414,10 +414,12 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { } // to be conservative - bitcoinFeeRate := max(ln.EstimateFee(), mempoolFeeRate) + bitcoinFeeRate := max(ln.EstimateFee(), mempoolFeeRate, bitcoin.EstimateSatvB(6)) + // shouil match peerswap estimation + swapFeeReserveBTC := uint64(math.Ceil(bitcoinFeeRate * 350)) // arbitrary haircut to avoid 'no matching outgoing channel available' - maxLiquidSwapIn := min(int64(satAmount)-int64(ln.SwapFeeReserveLBTC), int64(maxRemoteBalance)-10000) + maxLiquidSwapIn := min(int64(satAmount)-int64(swapFeeReserveLBTC), int64(maxRemoteBalance)-10000) if maxLiquidSwapIn < 100_000 { maxLiquidSwapIn = 0 } @@ -448,13 +450,13 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { } // arbitrary haircuts to avoid 'no matching outgoing channel available' - maxBitcoinSwapIn := min(btcBalance-int64(ln.SwapFeeReserveBTC), int64(maxRemoteBalance)-10000) + maxBitcoinSwapIn := min(btcBalance-int64(swapFeeReserveBTC), int64(maxRemoteBalance)-10000) if maxBitcoinSwapIn < 100_000 { maxBitcoinSwapIn = 0 } // assumed direction of the swap - directionIn := false + directionIn := true // assume return to 50/50 channel recommendLiquidSwapOut := uint64(0) @@ -484,9 +486,12 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { recommendLiquidSwapIn := int64(0) recommendBitcoinSwapIn := int64(0) if maxRemoteBalance > channelCapacity/2 { - directionIn = true recommendLiquidSwapIn = min(maxLiquidSwapIn, int64(maxRemoteBalance-channelCapacity/2)) recommendBitcoinSwapIn = min(maxBitcoinSwapIn, int64(maxRemoteBalance-channelCapacity/2)) + } else { + if recommendLiquidSwapOut > 0 { + directionIn = false + } } if recommendLiquidSwapIn < 100_000 { @@ -585,8 +590,8 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { KeysendSats: keysendSats, OutputsBTC: &utxosBTC, OutputsLBTC: &utxosLBTC, - ReserveLBTC: ln.SwapFeeReserveLBTC, - ReserveBTC: ln.SwapFeeReserveBTC, + ReserveLBTC: swapFeeReserveLBTC, + ReserveBTC: swapFeeReserveBTC, HasInboundFees: ln.HasInboundFees(), PeerBitcoinBalance: peerBitcoinBalance, MaxBitcoinSwapOut: maxBitcoinSwapOut, @@ -2270,44 +2275,6 @@ func submitHandler(w http.ResponseWriter, r *http.Request) { switch direction { case "in": - // reserve depends on asset and LND/CLN implementation - var reserve uint64 - var amountAvailable uint64 - - if asset == "btc" { - cl, clean, er := ln.GetClient() - if er != nil { - redirectWithError(w, r, "/config?", er) - return - } - defer clean() - - amountAvailable = uint64(ln.ConfirmedWalletBalance(cl)) - reserve = ln.SwapFeeReserveBTC - } else if asset == "lbtc" { - client, cleanup, err := ps.GetClient(config.Config.RpcHost) - if err != nil { - redirectWithError(w, r, "/config?", err) - return - } - defer cleanup() - - res, err := ps.LiquidGetBalance(client) - if err != nil { - log.Printf("unable to connect to RPC server: %v", err) - redirectWithError(w, r, "/config?", err) - return - } - - amountAvailable = res.GetSatAmount() - reserve = ln.SwapFeeReserveLBTC - } - - if amountAvailable < reserve || swapAmount > amountAvailable-reserve { - redirectWithError(w, r, "/peer?id="+nodeId+"&", errors.New("swap amount exceeds wallet balance less reserve "+formatWithThousandSeparators(reserve)+" sats")) - return - } - id, err = ps.SwapIn(client, swapAmount, channelId, asset, false) case "out": peerId := peerNodeId[channelId] diff --git a/cmd/psweb/cj/claimjoin.go b/cmd/psweb/ln/claimjoin.go similarity index 62% rename from cmd/psweb/cj/claimjoin.go rename to cmd/psweb/ln/claimjoin.go index e68eb97..60ff00f 100644 --- a/cmd/psweb/cj/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -1,17 +1,18 @@ -package cj +package ln import ( "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" + "encoding/json" "encoding/pem" "fmt" "log" + "time" "peerswap-web/cmd/psweb/config" "peerswap-web/cmd/psweb/db" - "peerswap-web/cmd/psweb/ln" "peerswap-web/cmd/psweb/ps" ) @@ -22,8 +23,50 @@ var ( pemToNodeId map[string]string // public key of the sender of pegin_started broadcast PeginHandler string + // when currently pending pegin can be claimed + ClaimBlockHeight uint32 + // human readable status of the claimjoin + ClaimStatus = "No third-party pegin is pending" + // none, initiator or joiner + MyRole = "none" + // array of initiator + joiners, for initiator only + claimParties []ClaimParty + // requires repeat of the last transmission + sayAgain = false ) +type Coordination struct { + // action required from the peer: "add", "remove", "process", "process2", "continue", "say_again" + Action string + // new joiner details + Joiner ClaimParty + // ETA of currently pending pegin claim + ClaimBlockHeight uint32 + // human readable status of the claimjoin + Status string + // partially signed elements transaction + PSET string +} + +type ClaimParty struct { + // pegin txid + TxId string + // pegin vout + Vout uint + // pegin claim script + ClaimScript string + // Liquid address to receive funds + Address string + // to be filled locally by initiator + RawTx string + TxoutProof string + Amount uint64 + FeeShare uint64 + PEM string + LastMessage *Coordination + SentTime time.Time +} + /* // first pegin peginTxId := "0b387c3b7a8d9fad4d7a1ac2cba4958451c03d6c4fe63dfbe10cdb86d666cdd7" @@ -89,11 +132,26 @@ var ( } */ -// Forward the announcement to all direct peers, unless the source PEM is known already -func BroadcastMessage(fromNodeId string, message *ln.Message) error { +// runs after restart, to continue if pegin is ongoing +func InitCJ() { + +} + +// runs every minute +func OnTimer() { + if sayAgain { + if SendCoordination(PeginHandler, &Coordination{Action: "say_again"}) == nil { + sayAgain = false + } + } +} + +// Forward the message to all direct peers, unless the source PEM is known already +// (it means the message came back to you from a downstream peer) +func Broadcast(fromNodeId string, message *Message) error { - if pemToNodeId[message.Sender] != "" { - // has been allready received from some nearer node + if message.Asset == "pegin_started" && pemToNodeId[message.Sender] != "" || message.Asset == "pegin_ended" && pemToNodeId == nil { + // has been previously received from upstream return nil } @@ -108,7 +166,7 @@ func BroadcastMessage(fromNodeId string, message *ln.Message) error { return err } - cl, clean, er := ln.GetClient() + cl, clean, er := GetClient() if er != nil { return err } @@ -117,7 +175,7 @@ func BroadcastMessage(fromNodeId string, message *ln.Message) error { for _, peer := range res.GetPeers() { // don't send back to where it came from if peer.NodeId != fromNodeId { - ln.SendCustomMessage(cl, peer.NodeId, message) + SendCustomMessage(cl, peer.NodeId, message) } } @@ -126,27 +184,39 @@ func BroadcastMessage(fromNodeId string, message *ln.Message) error { PeginHandler = message.Sender // store for relaying further encrypted messages pemToNodeId[message.Sender] = fromNodeId + // currently expected ETA is communicated via Amount + ClaimBlockHeight = uint32(message.Amount) + ClaimStatus = "Pegin started, awaiting joiners" } else if message.Asset == "pegin_ended" { PeginHandler = "" - // delete key data from the map - delete(pemToNodeId, message.Sender) + // delete the routing map + pemToNodeId = nil + ClaimBlockHeight = 0 + ClaimStatus = "No third-party pegin is pending" } // persist to db - db.Save("ClaimJoin", "PemToNodeId", &pemToNodeId) - db.Save("ClaimJoin", "PeginHandler", &PeginHandler) + db.Save("ClaimJoin", "pemToNodeId", pemToNodeId) + db.Save("ClaimJoin", "PeginHandler", PeginHandler) + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) return nil } -// Encrypt and send message to an anonymous peer identified only by his public key in PEM format +// Encrypt and send message to an anonymous peer identified only by public key in PEM format // New keys are generated at the start of each pegin session // Peers track sources of encrypted messages to forward back the replies -func SendEncryptedMessage(destinationPEM string, payload []byte) error { +func SendCoordination(destinationPEM string, message *Coordination) error { + + payload, err := json.Marshal(message) + if err != nil { + return err + } destinationNodeId := pemToNodeId[destinationPEM] if destinationNodeId == "" { - return fmt.Errorf("destination PEM has no matching NodeId") + return fmt.Errorf("Cannot send, destination PEM has no matching NodeId") } // Deserialize the received PEM string back to a public key @@ -163,21 +233,73 @@ func SendEncryptedMessage(destinationPEM string, payload []byte) error { return err } - cl, clean, er := ln.GetClient() + cl, clean, er := GetClient() if er != nil { return er } defer clean() - return ln.SendCustomMessage(cl, destinationNodeId, &ln.Message{ - Version: ln.MessageVersion, - Memo: "relay", + return SendCustomMessage(cl, destinationNodeId, &Message{ + Version: MessageVersion, + Memo: "process", Sender: myPublicPEM, Destination: destinationPEM, Payload: ciphertext, }) } +// Either forward to final distination or decrypt and process +func Process(message *Message) error { + + cl, clean, er := GetClient() + if er != nil { + return er + } + defer clean() + + if message.Destination == myPublicPEM { + // Decrypt the message using my private key + plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, myPrivateKey, message.Payload, nil) + if err != nil { + return fmt.Errorf("Error decrypting message:", err) + } + + // recover the struct + var msg Coordination + err = json.Unmarshal(plaintext, &msg) + if err != nil { + return fmt.Errorf("Received an incorrectly formed message:", err) + + } + + if msg.ClaimBlockHeight > ClaimBlockHeight { + ClaimBlockHeight = msg.ClaimBlockHeight + } + + switch msg.Action { + case "add": + if MyRole != "initiator" { + return fmt.Errorf("Cannot add a joiner, not a claim initiator") + } + + case "remove": + case "process": + case "process2": // process twice to blind and sign + + } + + return nil + } + + // relay further + destinationNodeId := pemToNodeId[message.Destination] + if destinationNodeId == "" { + return fmt.Errorf("Cannot relay, destination PEM has no matching NodeId") + } + return SendCustomMessage(cl, destinationNodeId, message) + +} + // generates new message keys func GenerateKeys() error { // Generate RSA key pair @@ -196,7 +318,7 @@ func GenerateKeys() error { } // persist to db - db.Save("ClaimJoin", "PrivateKey", &myPrivateKey) + db.Save("ClaimJoin", "PrivateKey", myPrivateKey) return nil } diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index 8be2198..a9a2344 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -28,10 +28,6 @@ import ( const ( Implementation = "CLN" fileRPC = "lightning-rpc" - // https://github.com/ElementsProject/peerswap/blob/master/clightning/clightning_commands.go#L392 - // 2000 to avoid high fee - SwapFeeReserveLBTC = uint64(2000) - SwapFeeReserveBTC = uint64(2000) ) type Forwarding struct { diff --git a/cmd/psweb/ln/common.go b/cmd/psweb/ln/common.go index 7b5f40e..8f438ed 100644 --- a/cmd/psweb/ln/common.go +++ b/cmd/psweb/ln/common.go @@ -321,6 +321,20 @@ func LoadDB() { return } } + + // init ClaimJoin + db.Load("ClaimJoin", "pemToNodeId", &pemToNodeId) + db.Load("ClaimJoin", "PeginHandler", &PeginHandler) + db.Load("ClaimJoin", "ClaimBlockHeight", &ClaimBlockHeight) + db.Load("ClaimJoin", "ClaimStatus", &ClaimStatus) + db.Load("ClaimJoin", "PrivateKey", &myPrivateKey) + db.Load("ClaimJoin", "MyRole", &MyRole) + db.Load("ClaimJoin", "claimParties", &claimParties) + + if MyRole == "joiner" && PeginHandler != "" { + // ask to send again the last transmission + sayAgain = true + } } func calculateAutoFee(channelId uint64, params *AutoFeeParams, liqPct int, oldFee int) int { diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index fad839b..07d6a07 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -20,7 +20,6 @@ import ( "time" "peerswap-web/cmd/psweb/bitcoin" - "peerswap-web/cmd/psweb/cj" "peerswap-web/cmd/psweb/config" "peerswap-web/cmd/psweb/db" @@ -45,10 +44,6 @@ import ( const ( Implementation = "LND" - // https://github.com/ElementsProject/peerswap/blob/master/peerswaprpc/server.go#L234 - // 2000 to avoid high fee - SwapFeeReserveLBTC = uint64(2000) - SwapFeeReserveBTC = uint64(2000) ) type InflightHTLC struct { @@ -1192,10 +1187,21 @@ func subscribeMessages(ctx context.Context, client lnrpc.LightningClient) error nodeId := hex.EncodeToString(data.Peer) - // received broadcast of begin status - // msg.Asset = "pegin_started" or "pegin_ended" + // received broadcast of pegin status + // msg.Asset: "pegin_started" or "pegin_ended" if msg.Memo == "broadcast" { - cj.BroadcastMessage(nodeId, &msg) + err = Broadcast(nodeId, &msg) + if err != nil { + log.Println(err) + } + } + + // messages related to pegin claim join + if msg.Memo == "process" { + err = Process(&msg) + if err != nil { + log.Println(err) + } } // received request for information diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 4f31e99..f898aa8 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -42,7 +42,8 @@ const ( swapOutChannelReserve = 5000 // https://github.com/ElementsProject/peerswap/blob/c77a82913d7898d0d3b7c83e4a990abf54bd97e5/swap/actions.go#L388 swapOutChainReserve = 20300 - // for Swap In reserves see /ln + // Swap In reserves + swapFeeReserveLBTC = uint64(300) ) type SwapParams struct { @@ -350,6 +351,9 @@ func onTimer(firstRun bool) { // Check if pegin can be claimed checkPegin() + // Handle ClaimJoin + go ln.OnTimer() + // check for updates go func() { t := internet.GetLatestTag() @@ -1089,6 +1093,11 @@ func checkPegin() { return } + if ln.MyRole != "none" { + // claim is handled by ClaimJoin + return + } + cl, clean, er := ln.GetClient() if er != nil { return @@ -1207,7 +1216,7 @@ func cacheAliases() { // To rebalance a channel with high enough historic fee PPM func findSwapInCandidate(candidate *SwapParams) error { // extra 1000 to avoid no-change tx spending all on fees - minAmount := config.Config.AutoSwapThresholdAmount - ln.SwapFeeReserveLBTC - 1000 + minAmount := config.Config.AutoSwapThresholdAmount - swapFeeReserveLBTC minPPM := config.Config.AutoSwapThresholdPPM client, cleanup, err := ps.GetClient(config.Config.RpcHost) @@ -1403,7 +1412,7 @@ func executeAutoSwap() { return } - amount = min(amount, satAmount-ln.SwapFeeReserveLBTC) + amount = min(amount, satAmount-swapFeeReserveLBTC) // execute swap id, err := ps.SwapIn(client, amount, candidate.ChannelId, "lbtc", false) diff --git a/go.mod b/go.mod index 6bddf96..1d5d1bd 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/btcsuite/btcd/btcutil/psbt v1.1.8 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 github.com/elementsproject/glightning v0.0.0-20240224063423-55240d61b52a - github.com/elementsproject/peerswap v0.2.98-0.20240408181051-0f0d34c6c506 + github.com/elementsproject/peerswap v0.2.98-0.20240802020201-5935fb465630 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/gorilla/mux v1.8.1 github.com/gorilla/sessions v1.2.2 @@ -47,8 +47,9 @@ require ( github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/btcsuite/winsvc v1.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/checksum0/go-electrum v0.0.0-20220912200153-b862ac442cf9 // indirect github.com/containerd/continuity v0.3.0 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.4.0 // indirect @@ -81,7 +82,9 @@ require ( github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.5 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect diff --git a/go.sum b/go.sum index 92a0766..74fe8cc 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= -github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= @@ -120,6 +120,8 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checksum0/go-electrum v0.0.0-20220912200153-b862ac442cf9 h1:PEkrrCdN0F0wgeof+V8dwMabAYccVBgJfqysVdlT51U= +github.com/checksum0/go-electrum v0.0.0-20220912200153-b862ac442cf9/go.mod h1:EjLxYzaf/28gOdSRlifeLfjoOA6aUjtJZhwaZPnjL9c= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -182,8 +184,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elementsproject/glightning v0.0.0-20240224063423-55240d61b52a h1:xnVQmVqGmSs3m8zPQF4iYEYiUAmJx8MlT9vJ3lAaOjc= github.com/elementsproject/glightning v0.0.0-20240224063423-55240d61b52a/go.mod h1:YAdIeSyx8VEhDCtEaGOJLmWNpPaQ3x4vYSAj9Vrppdo= -github.com/elementsproject/peerswap v0.2.98-0.20240408181051-0f0d34c6c506 h1:eAoDIDy2q7E2sMxTjBi2977dE96f5HPh9aExn2yJmIE= -github.com/elementsproject/peerswap v0.2.98-0.20240408181051-0f0d34c6c506/go.mod h1:H5XGBJ7MH+77hbB+158Za85htu51s/flsFAfuNbZh+Y= +github.com/elementsproject/peerswap v0.2.98-0.20240802020201-5935fb465630 h1:ikQ2e5X0Cdw16+YOkBfyfp/QSp7MnfA9xyw7IGlg6z0= +github.com/elementsproject/peerswap v0.2.98-0.20240802020201-5935fb465630/go.mod h1:QrSJ4zWkBJebDE2rdQ6HUoNtcIDTRvcRZB4JWnewnAU= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -332,8 +334,14 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= +github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -674,6 +682,8 @@ go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= From f4b05a1e0d693295379e8d4cd4dea5f2a57a27f1 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Mon, 5 Aug 2024 00:34:37 +0200 Subject: [PATCH 07/98] Initiate claimjoin --- .vscode/launch.json | 2 +- cmd/psweb/bitcoin/bitcoin.go | 15 ++++ cmd/psweb/config/common.go | 1 + cmd/psweb/ln/claimjoin.go | 129 +++++++++++++++++++++++++++++++++-- cmd/psweb/ln/cln.go | 18 +++-- cmd/psweb/ln/common.go | 13 +--- cmd/psweb/ln/lnd.go | 24 ++++--- cmd/psweb/main.go | 25 ++++++- 8 files changed, 191 insertions(+), 36 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index eab2a29..2c86c18 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,7 @@ "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - //"args": ["-datadir", "/home/vlad/.peerswap_t4"] + "args": ["-datadir", "/home/vlad/.peerswap_t4"] //"args": ["-datadir", "/home/vlad/.peerswap2"] } ] diff --git a/cmd/psweb/bitcoin/bitcoin.go b/cmd/psweb/bitcoin/bitcoin.go index cef6bf7..a3be10d 100644 --- a/cmd/psweb/bitcoin/bitcoin.go +++ b/cmd/psweb/bitcoin/bitcoin.go @@ -321,6 +321,21 @@ func DecodeRawTransaction(hexstring string) (*Transaction, error) { return &transaction, nil } +func FindVout(hexTx string, amount uint64) (uint, error) { + tx, err := DecodeRawTransaction(hexTx) + if err != nil { + return 0, err + } + + for i, o := range tx.Vout { + if uint64(o.Value*100_000_000) == amount { + return uint(i), nil + } + } + + return 0, fmt.Errorf("vout not found") +} + func SendRawTransaction(hexstring string) (string, error) { client := BitcoinClient() service := &Bitcoin{client} diff --git a/cmd/psweb/config/common.go b/cmd/psweb/config/common.go index bc294c3..0fef5d8 100644 --- a/cmd/psweb/config/common.go +++ b/cmd/psweb/config/common.go @@ -37,6 +37,7 @@ type Configuration struct { PeginAddress string PeginAmount int64 PeginFeeRate uint32 + PeginClaimJoin bool LightningDir string BitcoinHost string BitcoinUser string diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 60ff00f..1dd8237 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -11,6 +11,7 @@ import ( "log" "time" + "peerswap-web/cmd/psweb/bitcoin" "peerswap-web/cmd/psweb/config" "peerswap-web/cmd/psweb/db" "peerswap-web/cmd/psweb/ps" @@ -30,7 +31,7 @@ var ( // none, initiator or joiner MyRole = "none" // array of initiator + joiners, for initiator only - claimParties []ClaimParty + claimParties []*ClaimParty // requires repeat of the last transmission sayAgain = false ) @@ -57,6 +58,8 @@ type ClaimParty struct { ClaimScript string // Liquid address to receive funds Address string + // when can be claimed + ClaimBlockHeight uint32 // to be filled locally by initiator RawTx string TxoutProof string @@ -133,12 +136,26 @@ type ClaimParty struct { */ // runs after restart, to continue if pegin is ongoing -func InitCJ() { - +func loadClaimJoinDB() { + db.Load("ClaimJoin", "pemToNodeId", &pemToNodeId) + db.Load("ClaimJoin", "PeginHandler", &PeginHandler) + db.Load("ClaimJoin", "ClaimBlockHeight", &ClaimBlockHeight) + db.Load("ClaimJoin", "ClaimStatus", &ClaimStatus) + db.Load("ClaimJoin", "PrivateKey", &myPrivateKey) + db.Load("ClaimJoin", "MyRole", &MyRole) + db.Load("ClaimJoin", "claimParties", &claimParties) + + if MyRole == "joiner" && PeginHandler != "" { + // ask to send again the last transmission + sayAgain = true + } } // runs every minute func OnTimer() { + + // + if sayAgain { if SendCoordination(PeginHandler, &Coordination{Action: "say_again"}) == nil { sayAgain = false @@ -216,7 +233,8 @@ func SendCoordination(destinationPEM string, message *Coordination) error { destinationNodeId := pemToNodeId[destinationPEM] if destinationNodeId == "" { - return fmt.Errorf("Cannot send, destination PEM has no matching NodeId") + log.Println("Cannot send coordination, destination PEM has no matching NodeId") + return fmt.Errorf("destination PEM has no matching NodeId") } // Deserialize the received PEM string back to a public key @@ -261,17 +279,19 @@ func Process(message *Message) error { // Decrypt the message using my private key plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, myPrivateKey, message.Payload, nil) if err != nil { - return fmt.Errorf("Error decrypting message:", err) + return fmt.Errorf("Error decrypting message: %s", err) } // recover the struct var msg Coordination err = json.Unmarshal(plaintext, &msg) if err != nil { - return fmt.Errorf("Received an incorrectly formed message:", err) + return fmt.Errorf("Received an incorrectly formed message: %s", err) } + log.Println(msg) + if msg.ClaimBlockHeight > ClaimBlockHeight { ClaimBlockHeight = msg.ClaimBlockHeight } @@ -283,7 +303,7 @@ func Process(message *Message) error { } case "remove": - case "process": + case "process": // blind or sign case "process2": // process twice to blind and sign } @@ -357,3 +377,98 @@ func pemToPublicKey(pubPEM string) (*rsa.PublicKey, error) { return nil, fmt.Errorf("not an RSA public key") } } + +// called by claim join initiator after his pegin funding tx confirms +func InitiateClaimJoin(claimBlockHeight uint32) bool { + if myNodeId == "" { + // populates myNodeId + if getLndVersion() == 0 { + return false + } + } + + if Broadcast(myNodeId, &Message{ + Version: MessageVersion, + Memo: "broadcast", + Asset: "pegin_started", + Amount: uint64(claimBlockHeight), + }) == nil { + ClaimBlockHeight = claimBlockHeight + party := createClaimParty(claimBlockHeight) + if party != nil { + // initiate array of claim parties + claimParties = nil + claimParties[0] = party + // persist to db + db.Save("ClaimJoin", "claimParties", claimParties) + return true + } + } + return false +} + +// called by ClaimJoin joiner candidate after his pegin funding tx confirms +func JoinClaimJoin(claimBlockHeight uint32) bool { + if myNodeId == "" { + // populates myNodeId + if getLndVersion() == 0 { + return false + } + } + + party := createClaimParty(claimBlockHeight) + if party != nil { + if SendCoordination(PeginHandler, &Coordination{ + Action: "add", + Joiner: *party, + ClaimBlockHeight: claimBlockHeight, + }) == nil { + ClaimBlockHeight = claimBlockHeight + return true + } + } + return false +} + +func createClaimParty(claimBlockHeight uint32) *ClaimParty { + party := new(ClaimParty) + party.TxId = config.Config.PeginTxId + party.ClaimScript = config.Config.PeginClaimScript + party.ClaimBlockHeight = claimBlockHeight + party.Amount = uint64(config.Config.PeginAmount) + + var err error + party.RawTx, err = bitcoin.GetRawTransaction(config.Config.PeginTxId, nil) + if err != nil { + log.Println("Cannot create ClaimParty: GetRawTransaction:", err) + return nil + } + + party.Vout, err = bitcoin.FindVout(party.RawTx, uint64(config.Config.PeginAmount)) + if err != nil { + log.Println("Cannot create ClaimParty: FindVout:", err) + return nil + } + + party.TxoutProof, err = bitcoin.GetTxOutProof(config.Config.PeginTxId) + if err != nil { + log.Println("Cannot create ClaimParty: GetTxOutProof:", err) + return nil + } + + client, cleanup, err := ps.GetClient(config.Config.RpcHost) + if err != nil { + log.Println("Cannot create ClaimParty: GetClient:", err) + return nil + } + defer cleanup() + + res, err := ps.LiquidGetAddress(client) + if err != nil { + log.Println("Cannot create ClaimParty: LiquidGetAddress:", err) + return nil + } + party.Address = res.Address + + return party +} diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index a9a2344..a1f0824 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -141,16 +141,22 @@ func ListUnspent(client *glightning.Lightning, list *[]UTXO, minConfs int32) err return nil } - -// returns number of confirmations and whether the tx can be fee bumped -func GetTxConfirmations(client *glightning.Lightning, txid string) (int32, bool) { +func GetBlockHeight(client *glightning.Lightning) uint32 { res, err := client.GetInfo() if err != nil { log.Println("GetInfo:", err) - return 0, true + return 0 } - tip := int32(res.Blockheight) + tip := uint32(res.Blockheight) +} + +// returns number of confirmations and whether the tx can be fee bumped +func GetTxConfirmations(client *glightning.Lightning, txid string) (int32, bool) { + blockHeight := GetBlockHeight() + if blockHeight == 0 { + return 0, false + } height := internet.GetTxHeight(txid) @@ -164,7 +170,7 @@ func GetTxConfirmations(client *glightning.Lightning, txid string) (int32, bool) return result.Confirmations, true } - return tip - height, true + return int32(blockHeight) - height, true } func GetAlias(nodeKey string) string { diff --git a/cmd/psweb/ln/common.go b/cmd/psweb/ln/common.go index 8f438ed..f98419a 100644 --- a/cmd/psweb/ln/common.go +++ b/cmd/psweb/ln/common.go @@ -323,18 +323,7 @@ func LoadDB() { } // init ClaimJoin - db.Load("ClaimJoin", "pemToNodeId", &pemToNodeId) - db.Load("ClaimJoin", "PeginHandler", &PeginHandler) - db.Load("ClaimJoin", "ClaimBlockHeight", &ClaimBlockHeight) - db.Load("ClaimJoin", "ClaimStatus", &ClaimStatus) - db.Load("ClaimJoin", "PrivateKey", &myPrivateKey) - db.Load("ClaimJoin", "MyRole", &MyRole) - db.Load("ClaimJoin", "claimParties", &claimParties) - - if MyRole == "joiner" && PeginHandler != "" { - // ask to send again the last transmission - sayAgain = true - } + loadClaimJoinDB() } func calculateAutoFee(channelId uint64, params *AutoFeeParams, liqPct int, oldFee int) int { diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 07d6a07..b9da45b 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -669,11 +669,23 @@ func getLndVersion() float64 { } LndVerson += b / 100 + + // save myNodeId + myNodeId = res.GetIdentityPubkey() } return LndVerson } +func GetBlockHeight(client lnrpc.LightningClient) uint32 { + res, err := client.GetInfo(context.Background(), &lnrpc.GetInfoRequest{}) + if err != nil { + return 0 + } + + return res.GetBlockHeight() +} + func GetMyAlias() string { if myNodeAlias == "" { client, cleanup, err := GetClient() @@ -1838,12 +1850,10 @@ func applyAutoFee(client lnrpc.LightningClient, channelId uint64, htlcFail bool) ctx := context.Background() if myNodeId == "" { - // get my node id - res, err := client.GetInfo(ctx, &lnrpc.GetInfoRequest{}) - if err != nil { + // populates myNodeId + if getLndVersion() == 0 { return } - myNodeId = res.GetIdentityPubkey() } r, err := client.GetChanInfo(ctx, &lnrpc.ChanInfoRequest{ ChanId: channelId, @@ -1949,12 +1959,10 @@ func ApplyAutoFees() { ctx := context.Background() if myNodeId == "" { - // get my node id - res, err := client.GetInfo(ctx, &lnrpc.GetInfoRequest{}) - if err != nil { + // populates myNodeId + if getLndVersion() == 0 { return } - myNodeId = res.GetIdentityPubkey() } res, err := client.ListChannels(ctx, &lnrpc.ListChannelsRequest{ diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index f898aa8..d76a796 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -1119,8 +1119,8 @@ func checkPegin() { if config.Config.PeginClaimScript == "" { log.Println("BTC withdrawal complete, txId: " + config.Config.PeginTxId) telegramSendMessage("BTC withdrawal complete. TxId: `" + config.Config.PeginTxId + "`") - } else if confs > 101 { - // claim pegin + } else if confs > 101 && ln.MyRole == "none" { + // claim individual pegin failed := false proof := "" txid := "" @@ -1152,6 +1152,27 @@ func checkPegin() { telegramSendMessage("💸 Peg-in success! Liquid TxId: `" + txid + "`") } } else { + if config.Config.PeginClaimJoin { + if ln.MyRole == "none" { + currentBlockHeight := ln.GetBlockHeight(cl) + claimHeight := currentBlockHeight + 102 - uint32(confs) + if ln.PeginHandler == "" { + // I become pegin handler + if ln.InitiateClaimJoin(claimHeight) { + ln.MyRole = "initiator" + // persist to db + db.Save("ClaimJoin", "MyRole", ln.MyRole) + } + } else { + // join by replying to initiator + if ln.JoinClaimJoin(claimHeight) { + ln.MyRole = "joiner" + // persist to db + db.Save("ClaimJoin", "MyRole", ln.MyRole) + } + } + } + } return } From 9872d9d84c01af858f41125c1d222055607cafe3 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Mon, 5 Aug 2024 15:03:22 +0200 Subject: [PATCH 08/98] Fix cln --- .vscode/launch.json | 4 +- cmd/psweb/handlers.go | 114 ++++++++++++++++------------- cmd/psweb/liquid/elements.go | 13 ---- cmd/psweb/ln/claimjoin.go | 11 ++- cmd/psweb/ln/cln.go | 17 ++++- cmd/psweb/ln/lnd.go | 10 +++ cmd/psweb/ps/cln.go | 42 ++++++++++- cmd/psweb/templates/bitcoin.gohtml | 43 +++++++++-- 8 files changed, 173 insertions(+), 81 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2c86c18..41901b4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,11 +9,11 @@ "type": "go", "request": "launch", "mode": "auto", - "buildFlags": "-tags lnd", + "buildFlags": "-tags cln", "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - "args": ["-datadir", "/home/vlad/.peerswap_t4"] + //"args": ["-datadir", "/home/vlad/.peerswap_t4"] //"args": ["-datadir", "/home/vlad/.peerswap2"] } ] diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index cf7661d..a8c64d5 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -641,30 +641,32 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { defer clean() type Page struct { - Authenticated bool - ErrorMessage string - PopUpMessage string - ColorScheme string - BitcoinBalance uint64 - Outputs *[]ln.UTXO - PeginTxId string - IsPegin bool // false for ordinary BTC withdrawal - PeginAmount uint64 - BitcoinApi string - Confirmations int32 - Progress int32 - Duration string - FeeRate uint32 - LiquidFeeRate float64 - MempoolFeeRate float64 - SuggestedFeeRate uint32 - MinBumpFeeRate uint32 - CanBump bool - CanRBF bool - IsCLN bool - BitcoinAddress string - AdvertiseEnabled bool - BitcoinSwaps bool + Authenticated bool + ErrorMessage string + PopUpMessage string + ColorScheme string + BitcoinBalance uint64 + Outputs *[]ln.UTXO + PeginTxId string + IsPegin bool // false for ordinary BTC withdrawal + PeginAmount uint64 + BitcoinApi string + Confirmations int32 + Progress int32 + Duration string + FeeRate uint32 + LiquidFeeRate float64 + MempoolFeeRate float64 + SuggestedFeeRate uint32 + MinBumpFeeRate uint32 + CanBump bool + CanRBF bool + IsCLN bool + BitcoinAddress string + AdvertiseEnabled bool + BitcoinSwaps bool + HasDiscountedvSize bool + HasClaimJoinPending bool } btcBalance := ln.ConfirmedWalletBalance(cl) @@ -697,30 +699,32 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { ln.ListUnspent(cl, &utxos, minConfs) data := Page{ - Authenticated: config.Config.SecureConnection && config.Config.Password != "", - ErrorMessage: errorMessage, - PopUpMessage: popupMessage, - ColorScheme: config.Config.ColorScheme, - BitcoinBalance: uint64(btcBalance), - Outputs: &utxos, - PeginTxId: config.Config.PeginTxId, - IsPegin: config.Config.PeginClaimScript != "", - PeginAmount: uint64(config.Config.PeginAmount), - BitcoinApi: config.Config.BitcoinApi, - Confirmations: confs, - Progress: int32(confs * 100 / 102), - Duration: formattedDuration, - FeeRate: config.Config.PeginFeeRate, - MempoolFeeRate: mempoolFeeRate, - LiquidFeeRate: liquid.EstimateFee(), - SuggestedFeeRate: fee, - MinBumpFeeRate: config.Config.PeginFeeRate + 2, - CanBump: canBump, - CanRBF: ln.CanRBF(), - IsCLN: ln.Implementation == "CLN", - BitcoinAddress: addr, - AdvertiseEnabled: ln.AdvertiseBitcoinBalance, - BitcoinSwaps: config.Config.BitcoinSwaps, + Authenticated: config.Config.SecureConnection && config.Config.Password != "", + ErrorMessage: errorMessage, + PopUpMessage: popupMessage, + ColorScheme: config.Config.ColorScheme, + BitcoinBalance: uint64(btcBalance), + Outputs: &utxos, + PeginTxId: config.Config.PeginTxId, + IsPegin: config.Config.PeginClaimScript != "", + PeginAmount: uint64(config.Config.PeginAmount), + BitcoinApi: config.Config.BitcoinApi, + Confirmations: confs, + Progress: int32(confs * 100 / 102), + Duration: formattedDuration, + FeeRate: config.Config.PeginFeeRate, + MempoolFeeRate: mempoolFeeRate, + LiquidFeeRate: liquid.EstimateFee(), + SuggestedFeeRate: fee, + MinBumpFeeRate: config.Config.PeginFeeRate + 2, + CanBump: canBump, + CanRBF: ln.CanRBF(), + IsCLN: ln.Implementation == "CLN", + BitcoinAddress: addr, + AdvertiseEnabled: ln.AdvertiseBitcoinBalance, + BitcoinSwaps: config.Config.BitcoinSwaps, + HasDiscountedvSize: hasDiscountedvSize, + HasClaimJoinPending: ln.PeginHandler != "", } // executing template named "bitcoin" @@ -798,10 +802,14 @@ func peginHandler(w http.ResponseWriter, r *http.Request) { claimScript := "" if isPegin { - // test on pre-existing tx that bitcon core can complete the peg + // test on a pre-existing tx that bitcon core can complete the peg tx := "b61ec844027ce18fd3eb91fa7bed8abaa6809c4d3f6cf4952b8ebaa7cd46583a" if config.Config.Chain == "testnet" { - tx = "2c7ec5043fe8ee3cb4ce623212c0e52087d3151c9e882a04073cce1688d6fc1e" + if hasDiscountedvSize { + tx = "0b387c3b7a8d9fad4d7a1ac2cba4958451c03d6c4fe63dfbe10cdb86d666cdd7" + } else { + tx = "2c7ec5043fe8ee3cb4ce623212c0e52087d3151c9e882a04073cce1688d6fc1e" + } } _, err = bitcoin.GetTxOutProof(tx) @@ -867,6 +875,12 @@ func peginHandler(w http.ResponseWriter, r *http.Request) { config.Config.PeginReplacedTxId = "" config.Config.PeginFeeRate = uint32(fee) + if hasDiscountedvSize { + config.Config.PeginClaimJoin = r.FormValue("claimJoin") == "true" + } else { + config.Config.PeginClaimJoin = false + } + if err := config.Save(); err != nil { redirectWithError(w, r, "/bitcoin?", err) return diff --git a/cmd/psweb/liquid/elements.go b/cmd/psweb/liquid/elements.go index 5a7149c..da1b5f7 100644 --- a/cmd/psweb/liquid/elements.go +++ b/cmd/psweb/liquid/elements.go @@ -14,7 +14,6 @@ import ( "time" "peerswap-web/cmd/psweb/config" - "peerswap-web/cmd/psweb/ln" "github.com/alexmullins/zip" ) @@ -325,18 +324,6 @@ type PeginAddress struct { func GetPeginAddress(address *PeginAddress) error { - if config.Config.Chain == "testnet" { - // to not waste testnet sats where pegin is not implemented - // return new P2TR address in our own bitcoin wallet - addr, err := ln.NewAddress() - if err != nil { - return err - } - address.ClaimScript = "peg-in is not implemented on testnet" - address.MainChainAddress = addr - return nil - } - client := ElementsClient() service := &Elements{client} wallet := config.Config.ElementsWallet diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 1dd8237..859b19f 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -1,3 +1,5 @@ +//go:build !cln + package ln import ( @@ -163,6 +165,7 @@ func OnTimer() { } } +// Called when received a broadcast custom message // Forward the message to all direct peers, unless the source PEM is known already // (it means the message came back to you from a downstream peer) func Broadcast(fromNodeId string, message *Message) error { @@ -279,14 +282,14 @@ func Process(message *Message) error { // Decrypt the message using my private key plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, myPrivateKey, message.Payload, nil) if err != nil { - return fmt.Errorf("Error decrypting message: %s", err) + return fmt.Errorf("error decrypting message: %s", err) } // recover the struct var msg Coordination err = json.Unmarshal(plaintext, &msg) if err != nil { - return fmt.Errorf("Received an incorrectly formed message: %s", err) + return fmt.Errorf("received an incorrectly formed message: %s", err) } @@ -299,7 +302,7 @@ func Process(message *Message) error { switch msg.Action { case "add": if MyRole != "initiator" { - return fmt.Errorf("Cannot add a joiner, not a claim initiator") + return fmt.Errorf("cannot add a joiner, not a claim initiator") } case "remove": @@ -314,7 +317,7 @@ func Process(message *Message) error { // relay further destinationNodeId := pemToNodeId[message.Destination] if destinationNodeId == "" { - return fmt.Errorf("Cannot relay, destination PEM has no matching NodeId") + return fmt.Errorf("cannot relay, destination PEM has no matching NodeId") } return SendCustomMessage(cl, destinationNodeId, message) diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index a1f0824..b79f9e0 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -148,12 +148,12 @@ func GetBlockHeight(client *glightning.Lightning) uint32 { return 0 } - tip := uint32(res.Blockheight) + return uint32(res.Blockheight) } // returns number of confirmations and whether the tx can be fee bumped func GetTxConfirmations(client *glightning.Lightning, txid string) (int32, bool) { - blockHeight := GetBlockHeight() + blockHeight := GetBlockHeight(client) if blockHeight == 0 { return 0, false } @@ -163,7 +163,7 @@ func GetTxConfirmations(client *glightning.Lightning, txid string) (int32, bool) if height == 0 { // mempool api error, use bitcoin core var result bitcoin.Transaction - _, err = bitcoin.GetRawTransaction(txid, &result) + _, err := bitcoin.GetRawTransaction(txid, &result) if err != nil { return -1, true // signal tx not found } @@ -1310,3 +1310,14 @@ func SendCustomMessage(client *glightning.Lightning, peerId string, message *Mes return nil } + +// ClaimJoin with CLN in not implemented, placeholder functions and variables: +func loadClaimJoinDB() {} +func OnTimer() {} +func InitiateClaimJoin(claimHeight uint32) bool { return false } +func JoinClaimJoin(claimHeight uint32) bool { return false } + +var ( + PeginHandler = "" + MyRole = "none" +) diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index b9da45b..f85b25c 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -1218,6 +1218,16 @@ func subscribeMessages(ctx context.Context, client lnrpc.LightningClient) error // received request for information if msg.Memo == "poll" { + if MyRole == "initiator" { + // repeat pegin start broadcast + SendCustomMessage(client, nodeId, &Message{ + Version: MessageVersion, + Memo: "broadcast", + Asset: "pegin_started", + Amount: uint64(ClaimBlockHeight), + }) + } + if AdvertiseLiquidBalance { if SendCustomMessage(client, nodeId, &Message{ Version: MessageVersion, diff --git a/cmd/psweb/ps/cln.go b/cmd/psweb/ps/cln.go index e460efe..9db4218 100644 --- a/cmd/psweb/ps/cln.go +++ b/cmd/psweb/ps/cln.go @@ -6,7 +6,8 @@ import ( "fmt" "log" "peerswap-web/cmd/psweb/config" - "peerswap-web/cmd/psweb/ln" + "strconv" + "strings" "github.com/elementsproject/glightning/glightning" "github.com/elementsproject/peerswap/clightning" @@ -97,7 +98,7 @@ func ListPeers(client *glightning.Lightning) (*peerswaprpc.ListPeersResponse, er for _, channel := range channels { channelData := channel.(map[string]interface{}) peer.Channels = append(peer.Channels, &peerswaprpc.PeerSwapPeerChannel{ - ChannelId: ln.ConvertClnToLndChannelId(channelData["short_channel_id"].(string)), + ChannelId: convertClnToLndChannelId(channelData["short_channel_id"].(string)), LocalBalance: uint64(channelData["local_balance"].(float64)), RemoteBalance: uint64(channelData["remote_balance"].(float64)), Active: channelData["state"].(string) == "CHANNELD_NORMAL", @@ -262,7 +263,7 @@ func SwapIn(client *glightning.Lightning, swapAmount, channelId uint64, asset st var res map[string]interface{} err := client.Request(&clightning.SwapIn{ - ShortChannelId: ln.ConvertLndToClnChannelId(channelId), + ShortChannelId: convertLndToClnChannelId(channelId), SatAmt: swapAmount, Asset: asset, Force: force, @@ -279,7 +280,7 @@ func SwapOut(client *glightning.Lightning, swapAmount, channelId uint64, asset s var res map[string]interface{} err := client.Request(&clightning.SwapOut{ - ShortChannelId: ln.ConvertLndToClnChannelId(channelId), + ShortChannelId: convertLndToClnChannelId(channelId), SatAmt: swapAmount, Asset: asset, Force: force, @@ -310,3 +311,36 @@ func AllowSwapRequests(client *glightning.Lightning, allowSwapRequests bool) (*p // return empty object as it is not used return &peerswaprpc.Policy{}, nil } + +// convert short channel id 2568777x70x1 to LND format +func convertClnToLndChannelId(s string) uint64 { + parts := strings.Split(s, "x") + if len(parts) != 3 { + return 0 // or handle error appropriately + } + + var scid uint64 + for i, part := range parts { + val, err := strconv.Atoi(part) + if err != nil { + return 0 // or handle error appropriately + } + switch i { + case 0: + scid |= uint64(val) << 40 + case 1: + scid |= uint64(val) << 16 + case 2: + scid |= uint64(val) + } + } + return scid +} + +// convert LND channel id to CLN 2568777x70x1 +func convertLndToClnChannelId(s uint64) string { + block := strconv.FormatUint(s>>40, 10) + tx := strconv.FormatUint((s>>16)&0xFFFFFF, 10) + output := strconv.FormatUint(s&0xFFFF, 10) + return block + "x" + tx + "x" + output +} diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index d78fc3e..9ae0b28 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -67,6 +67,25 @@ + {{if .HasDiscountedvSize}} +
+
+ ClaimJoin +
+
+
+ +
+
+
+ {{end}}
@@ -607,9 +626,14 @@ if (isPegin) { //liquid peg-in fee estimate - liquidFee = Math.ceil({{.LiquidFeeRate}} * 450) - fee += liquidFee; - text += "Liquid chain fee: " + formatWithThousandSeparators(liquidFee) + " sats\n"; + let liquidFee = "45"; + fee += 45; + {{if .HasDiscountedvSize}} + if (document.getElementById("claimJoin").checked) { + liquidFee = "32-45" + } + {{end}} + text += "Liquid chain fee: " + liquidFee + " sats\n"; } document.getElementById("totalFee").value = Number(fee); @@ -620,9 +644,18 @@ netAmount -= fee; } - text += "Estimated total fee: " + formatWithThousandSeparators(fee) + " sats\n"; - text += "Estimated PPM: " + formatWithThousandSeparators(Math.round(fee * 1000000 / netAmount)); + text += "Total fee: " + formatWithThousandSeparators(fee) + " sats\n"; + text += "Cost PPM: " + formatWithThousandSeparators(Math.round(fee * 1000000 / netAmount)); + let hours = "17 hours"; + {{if .HasDiscountedvSize}} + if (document.getElementById("claimJoin").checked) { + hours = "17-34 hours"; + } + {{end}} + + text += "\nClaim ETA: " + hours; + if (subtractFee && {{.BitcoinBalance}} - peginAmount < 25000) { {{if .IsCLN}} text += "\nReserve for anchor fee bumping will be returned as change." From 36088a89012a6b597ffaa330b8ad932582e07140 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Wed, 7 Aug 2024 22:08:07 +0200 Subject: [PATCH 09/98] Progress --- .env | 3 +- .vscode/launch.json | 4 +- cmd/psweb/handlers.go | 43 +- cmd/psweb/liquid/elements.go | 302 ++++++--- cmd/psweb/ln/claimjoin.go | 1000 ++++++++++++++++++++++------ cmd/psweb/ln/cln.go | 8 +- cmd/psweb/ln/common.go | 8 +- cmd/psweb/ln/lnd.go | 12 +- cmd/psweb/main.go | 87 ++- cmd/psweb/telegram.go | 33 +- cmd/psweb/templates/bitcoin.gohtml | 36 +- go.mod | 10 +- go.sum | 12 +- 13 files changed, 1222 insertions(+), 336 deletions(-) diff --git a/.env b/.env index e86e97f..df35131 100644 --- a/.env +++ b/.env @@ -1,3 +1,2 @@ DEBUG=1 -NETWORK="testnet" -NO_HTTPS="true" \ No newline at end of file +NETWORK="testnet" \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 41901b4..2c86c18 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,11 +9,11 @@ "type": "go", "request": "launch", "mode": "auto", - "buildFlags": "-tags cln", + "buildFlags": "-tags lnd", "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - //"args": ["-datadir", "/home/vlad/.peerswap_t4"] + "args": ["-datadir", "/home/vlad/.peerswap_t4"] //"args": ["-datadir", "/home/vlad/.peerswap2"] } ] diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index a8c64d5..ef332f1 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -632,7 +632,6 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { addr = keys[0] } - var utxos []ln.UTXO cl, clean, er := ln.GetClient() if er != nil { redirectWithError(w, r, "/config?", er) @@ -652,6 +651,7 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { PeginAmount uint64 BitcoinApi string Confirmations int32 + TargetConfirmations int32 Progress int32 Duration string FeeRate uint32 @@ -666,16 +666,21 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { AdvertiseEnabled bool BitcoinSwaps bool HasDiscountedvSize bool + CanClaimJoin bool + IsClaimJoin bool + ClaimJoinStatus string HasClaimJoinPending bool } btcBalance := ln.ConfirmedWalletBalance(cl) fee := uint32(mempoolFeeRate) confs := int32(0) - minConfs := int32(1) canBump := false canCPFP := false + var utxos []ln.UTXO + ln.ListUnspent(cl, &utxos, int32(1)) + if config.Config.PeginTxId != "" { confs, canCPFP = ln.GetTxConfirmations(cl, config.Config.PeginTxId) if confs == 0 { @@ -694,9 +699,22 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { } } - duration := time.Duration(10*(102-confs)) * time.Minute + duration := time.Duration(10*(ln.PeginBlocks-confs)) * time.Minute + maxConfs := int32(ln.PeginBlocks) + + if config.Config.PeginClaimJoin && ln.MyRole != "none" { + bh := int32(ln.GetBlockHeight(cl)) + target := int32(ln.ClaimBlockHeight) + maxConfs = target - bh + confs + duration = time.Duration(10*(target-bh)) * time.Minute + } + + progress := confs * 100 / int32(maxConfs) + formattedDuration := time.Time{}.Add(duration).Format("15h 04m") - ln.ListUnspent(cl, &utxos, minConfs) + if duration < 0 { + formattedDuration = "Past due" + } data := Page{ Authenticated: config.Config.SecureConnection && config.Config.Password != "", @@ -710,7 +728,8 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { PeginAmount: uint64(config.Config.PeginAmount), BitcoinApi: config.Config.BitcoinApi, Confirmations: confs, - Progress: int32(confs * 100 / 102), + TargetConfirmations: maxConfs, + Progress: progress, Duration: formattedDuration, FeeRate: config.Config.PeginFeeRate, MempoolFeeRate: mempoolFeeRate, @@ -723,7 +742,9 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { BitcoinAddress: addr, AdvertiseEnabled: ln.AdvertiseBitcoinBalance, BitcoinSwaps: config.Config.BitcoinSwaps, - HasDiscountedvSize: hasDiscountedvSize, + CanClaimJoin: hasDiscountedvSize && ln.Implementation == "LND", + IsClaimJoin: config.Config.PeginClaimJoin, + ClaimJoinStatus: ln.ClaimStatus, HasClaimJoinPending: ln.PeginHandler != "", } @@ -860,7 +881,7 @@ func peginHandler(w http.ResponseWriter, r *http.Request) { if isPegin { log.Println("Peg-in TxId:", res.TxId, "RawHex:", res.RawHex, "Claim script:", claimScript) - duration := time.Duration(1020) * time.Minute + duration := time.Duration(10*ln.PeginBlocks) * time.Minute formattedDuration := time.Time{}.Add(duration).Format("15h 04m") telegramSendMessage("⏰ Started peg-in " + formatWithThousandSeparators(uint64(res.AmountSat)) + " sats. Time left: " + formattedDuration + ". TxId: `" + res.TxId + "`") } else { @@ -875,8 +896,12 @@ func peginHandler(w http.ResponseWriter, r *http.Request) { config.Config.PeginReplacedTxId = "" config.Config.PeginFeeRate = uint32(fee) - if hasDiscountedvSize { - config.Config.PeginClaimJoin = r.FormValue("claimJoin") == "true" + if hasDiscountedvSize && ln.Implementation == "LND" { + config.Config.PeginClaimJoin = r.FormValue("claimJoin") == "on" + if config.Config.PeginClaimJoin { + ln.ClaimStatus = "Awaiting tx confirmation" + db.Save("ClaimJoin", "ClaimStatus", ln.ClaimStatus) + } } else { config.Config.PeginClaimJoin = false } diff --git a/cmd/psweb/liquid/elements.go b/cmd/psweb/liquid/elements.go index da1b5f7..74cc0cd 100644 --- a/cmd/psweb/liquid/elements.go +++ b/cmd/psweb/liquid/elements.go @@ -219,7 +219,7 @@ func SendToAddress(address string, params := &SendParams{ Address: address, - Amount: toBitcoin(amountSats), + Amount: ToBitcoin(amountSats), Comment: comment, SubtractFeeFromAmount: subtractFeeFromAmount, Replaceable: replaceable, @@ -365,7 +365,7 @@ func ClaimPegin(rawTx, proof, claimScript string) (string, error) { return txid, nil } -func toBitcoin(amountSats uint64) float64 { +func ToBitcoin(amountSats uint64) float64 { return float64(amountSats) / float64(100_000_000) } @@ -428,64 +428,14 @@ func HasDiscountedvSize() bool { return response["version"].(float64) >= 230202 } -func CreateClaimPSBT(peginTxId string, - peginVout uint, - peginRawTx string, - peginTxoutProof string, - peginClaimScript string, - peginAmount uint64, - liquidAddress string, - peginTxId2 string, - peginVout2 uint, - peginRawTx2 string, - peginTxoutProof2 string, - peginClaimScript2 string, - peginAmount2 uint64, - liquidAddress2 string, - fee uint64) (string, error) { +func CreatePSET(params interface{}) (string, error) { client := ElementsClient() service := &Elements{client} - // Create the inputs array - inputs := []map[string]interface{}{ - { - "txid": peginTxId, - "vout": peginVout, - "pegin_bitcoin_tx": peginRawTx, - "pegin_txout_proof": peginTxoutProof, - "pegin_claim_script": peginClaimScript, - }, - { - "txid": peginTxId2, - "vout": peginVout2, - "pegin_bitcoin_tx": peginRawTx2, - "pegin_txout_proof": peginTxoutProof2, - "pegin_claim_script": peginClaimScript2, - }, - } - - // Create the outputs array - outputs := []map[string]interface{}{ - { - liquidAddress: toBitcoin(peginAmount), - "blinder_index": 0, - }, - { - liquidAddress2: toBitcoin(peginAmount2), - "blinder_index": 1, - }, - { - "fee": toBitcoin(fee), - }, - } - - // Combine inputs and outputs into the parameters array - params := []interface{}{inputs, outputs} - r, err := service.client.call("createpsbt", params, "") if err = handleError(err, &r); err != nil { - log.Printf("Failed to create raw transaction: %v", err) + log.Printf("Failed to create PSET: %v", err) return "", err } @@ -493,14 +443,14 @@ func CreateClaimPSBT(peginTxId string, err = json.Unmarshal([]byte(r.Result), &response) if err != nil { - log.Printf("CreateClaimRawTx unmarshall: %v", err) + log.Printf("CreatePSET unmarshall: %v", err) return "", err } return response, nil } -func ProcessPSBT(base64psbt, wallet string) (string, bool, error) { +func ProcessPSET(base64psbt, wallet string) (string, bool, error) { client := ElementsClient() service := &Elements{client} @@ -509,7 +459,7 @@ func ProcessPSBT(base64psbt, wallet string) (string, bool, error) { r, err := service.client.call("walletprocesspsbt", params, "/wallet/"+wallet) if err = handleError(err, &r); err != nil { - log.Printf("Failed to process PSBT: %v", err) + log.Printf("Failed to process PSET: %v", err) return "", false, err } @@ -517,61 +467,261 @@ func ProcessPSBT(base64psbt, wallet string) (string, bool, error) { err = json.Unmarshal([]byte(r.Result), &response) if err != nil { - log.Printf("ProcessPSBT unmarshall: %v", err) + log.Printf("ProcessPSET unmarshall: %v", err) return "", false, err } return response["psbt"].(string), response["complete"].(bool), nil } -func CombinePSBT(psbt []string) (string, error) { +func FinalizePSET(psbt string) (string, bool, error) { client := ElementsClient() service := &Elements{client} params := []interface{}{psbt} - r, err := service.client.call("combinepsbt", params, "") + r, err := service.client.call("finalizepsbt", params, "") if err = handleError(err, &r); err != nil { - log.Printf("Failed to combine PSBT: %v", err) - return "", err + log.Printf("Failed to finalize PSET: %v", err) + return "", false, err } - var response string + var response map[string]interface{} err = json.Unmarshal([]byte(r.Result), &response) if err != nil { - log.Printf("CombinePSBT unmarshall: %v", err) - return "", err + log.Printf("FinalizePSET unmarshall: %v", err) + return "", false, err } - return response, nil + if response["complete"].(bool) { + return response["hex"].(string), true, nil + } + + return response["psbt"].(string), false, nil +} + +type Missing struct { + Pubkeys []string `json:"pubkeys"` +} + +type AnalyzeInput struct { + HasUTXO bool `json:"has_utxo"` + IsFinal bool `json:"is_final"` + Next string `json:"next"` + Missing Missing `json:"missing"` +} + +type AnalyzeOutput struct { + Blind bool `json:"blind"` + Status string `json:"status"` +} + +type AnalyzedPSET struct { + Inputs []AnalyzeInput `json:"inputs"` + Outputs []AnalyzeOutput `json:"outputs"` + Fee float64 `json:"fee"` + Next string `json:"next"` } -func FinalizePSBT(psbt string) (string, bool, error) { +func AnalyzePSET(psbt string) (*AnalyzedPSET, error) { client := ElementsClient() service := &Elements{client} params := []interface{}{psbt} - r, err := service.client.call("finalizepsbt", params, "") + r, err := service.client.call("analyzepsbt", params, "") if err = handleError(err, &r); err != nil { - log.Printf("Failed to finalize PSBT: %v", err) - return "", false, err + log.Printf("Failed to analyze PSET: %v", err) + return nil, err } - var response map[string]interface{} + var response AnalyzedPSET err = json.Unmarshal([]byte(r.Result), &response) if err != nil { - log.Printf("FinalizePSBT unmarshall: %v", err) - return "", false, err + log.Printf("AnalyzePSET unmarshall: %v", err) + return nil, err } - if response["complete"].(bool) { - return response["hex"].(string), true, nil + return &response, nil +} + +type DecodedScript struct { + Asm string `json:"asm"` + Desc string `json:"desc"` + Hex string `json:"hex"` + Address string `json:"address,omitempty"` + Type string `json:"type"` +} + +type DecodedOutput struct { + Amount float64 `json:"amount"` + Script DecodedScript `json:"script"` + Asset string `json:"asset"` + BlindingPubKey string `json:"blinding_pubkey,omitempty"` + BlinderIndex int `json:"blinder_index,omitempty"` + Status string `json:"status,omitempty"` +} + +type DecodedInput struct { + PreviousTxid string `json:"previous_txid"` + PreviousVout int `json:"previous_vout"` + Sequence uint32 `json:"sequence"` + PeginBitcoinTx string `json:"pegin_bitcoin_tx"` + PeginTxoutProof string `json:"pegin_txout_proof"` + PeginClaimScript string `json:"pegin_claim_script"` + PeginGenesisHash string `json:"pegin_genesis_hash"` + PeginValue float64 `json:"pegin_value"` + FinalScriptWitness []string `json:"final_scriptwitness,omitempty"` +} + +type DecodedFees struct { + Bitcoin float64 `json:"bitcoin"` +} + +type DecodedPSET struct { + GlobalXpubs []interface{} `json:"global_xpubs"` + TxVersion int `json:"tx_version"` + FallbackLocktime int `json:"fallback_locktime"` + InputCount int `json:"input_count"` + OutputCount int `json:"output_count"` + PsbtVersion int `json:"psbt_version"` + Proprietary []interface{} `json:"proprietary"` + Fees DecodedFees `json:"fees"` + Unknown map[string]interface{} `json:"unknown"` + Inputs []DecodedInput `json:"inputs"` + Outputs []DecodedOutput `json:"outputs"` +} + +func DecodePSET(psbt string) (*DecodedPSET, error) { + + client := ElementsClient() + service := &Elements{client} + + params := []interface{}{psbt} + + r, err := service.client.call("decodepsbt", params, "") + if err = handleError(err, &r); err != nil { + log.Printf("Failed to decode PSET: %v", err) + return nil, err } - return response["psbt"].(string), false, nil + var response DecodedPSET + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("DecodePSET unmarshall: %v", err) + return nil, err + } + + return &response, nil +} + +type ScriptSig struct { + Asm string `json:"asm"` + Hex string `json:"hex"` +} + +type TxinWitness []string +type PeginWitness []string + +type Vin struct { + Txid string `json:"txid"` + Vout int `json:"vout"` + ScriptSig ScriptSig `json:"scriptSig"` + IsPegin bool `json:"is_pegin"` + Sequence uint32 `json:"sequence"` + TxinWitness TxinWitness `json:"txinwitness"` + PeginWitness PeginWitness `json:"pegin_witness"` +} + +type ScriptPubKey struct { + Asm string `json:"asm"` + Desc string `json:"desc"` + Hex string `json:"hex"` + Address string `json:"address"` + Type string `json:"type"` +} + +type Vout struct { + ValueMinimum float64 `json:"value-minimum"` + ValueMaximum float64 `json:"value-maximum"` + CtExponent int `json:"ct-exponent"` + CtBits int `json:"ct-bits"` + SurjectionProof string `json:"surjectionproof"` + ValueCommitment string `json:"valuecommitment"` + AssetCommitment string `json:"assetcommitment"` + CommitmentNonce string `json:"commitmentnonce"` + CommitmentNonceValid bool `json:"commitmentnonce_fully_valid"` + N int `json:"n"` + ScriptPubKey ScriptPubKey `json:"scriptPubKey"` + Value float64 `json:"value,omitempty"` + Asset string `json:"asset,omitempty"` +} + +type Transaction struct { + Txid string `json:"txid"` + Hash string `json:"hash"` + Wtxid string `json:"wtxid"` + Withash string `json:"withash"` + Version int `json:"version"` + Size int `json:"size"` + Vsize int `json:"vsize"` + DiscountVsize int `json:"discountvsize"` + Weight int `json:"weight"` + Locktime int `json:"locktime"` + Vin []Vin `json:"vin"` + Vout []Vout `json:"vout"` + Fee map[string]float64 `json:"fee"` +} + +func DecodeRawTransaction(hexTx string) (*Transaction, error) { + + client := ElementsClient() + service := &Elements{client} + + params := []interface{}{hexTx} + + r, err := service.client.call("decoderawtransaction", params, "") + if err = handleError(err, &r); err != nil { + log.Printf("Failed to decode raw tx: %v", err) + return nil, err + } + + var response Transaction + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("DecodeRawTransaction unmarshall: %v", err) + return nil, err + } + + return &response, nil +} + +func SendRawTransaction(hexTx string) (string, error) { + + client := ElementsClient() + service := &Elements{client} + + params := []interface{}{hexTx} + + r, err := service.client.call("sendrawtransaction", params, "") + if err = handleError(err, &r); err != nil { + log.Printf("Failed to send raw tx: %v", err) + return "", err + } + + var response string + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("SendRawTransaction unmarshall: %v", err) + return "", err + } + + return response, nil } diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 859b19f..dc923ce 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -4,42 +4,56 @@ package ln import ( "crypto/rand" - "crypto/rsa" "crypto/sha256" - "crypto/x509" + "encoding/base64" "encoding/json" - "encoding/pem" "fmt" + "io" "log" + "strconv" "time" + mathRand "math/rand" + + "github.com/btcsuite/btcd/btcec/v2" + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/hkdf" + "peerswap-web/cmd/psweb/bitcoin" "peerswap-web/cmd/psweb/config" "peerswap-web/cmd/psweb/db" + "peerswap-web/cmd/psweb/internet" + "peerswap-web/cmd/psweb/liquid" "peerswap-web/cmd/psweb/ps" ) +// maximum number of participants in ClaimJoin +const ( + maxParties = 2 + PeginBlocks = 10 //2 +) + var ( - myPrivateKey *rsa.PrivateKey - myPublicPEM string + // encryption private key + myPrivateKey *btcec.PrivateKey // maps learned public keys to node Id - pemToNodeId map[string]string + keyToNodeId = make(map[string]string) // public key of the sender of pegin_started broadcast PeginHandler string // when currently pending pegin can be claimed ClaimBlockHeight uint32 // human readable status of the claimjoin - ClaimStatus = "No third-party pegin is pending" + ClaimStatus = "No ClaimJoin peg-in is pending" // none, initiator or joiner MyRole = "none" // array of initiator + joiners, for initiator only claimParties []*ClaimParty - // requires repeat of the last transmission - sayAgain = false + // PSET to be blinded and signed by all parties + claimPSET string ) type Coordination struct { - // action required from the peer: "add", "remove", "process", "process2", "continue", "say_again" + // possible actions: add, confirm_add, refuse_add, process, process2, remove Action string // new joiner details Joiner ClaimParty @@ -67,110 +81,209 @@ type ClaimParty struct { TxoutProof string Amount uint64 FeeShare uint64 - PEM string + PubKey string LastMessage *Coordination SentTime time.Time } -/* - // first pegin - peginTxId := "0b387c3b7a8d9fad4d7a1ac2cba4958451c03d6c4fe63dfbe10cdb86d666cdd7" - peginVout := uint(0) - peginRawTx := "0200000000010159c1a062851325f301282b8ae1124c98228ea7b100eb82e907c6c314001a11860100000000fdffffff03a08601000000000017a914c74490c48b105376e697925c314c47afb00f1303872b1400000000000017a914b5e28484d1f1a5d74878f6ef411d555ac170e62887400d03000000000017a9146ec12f7b07420d693b59c8e6c20b17fbe40503ae87024730440220687049a9caf086d6a2205534acde99e549bde1bb922cfb6f7a7f5204a48ccebc02201c0d84dff34c3ce455fc6806015c160fa35eee86d2fddc7ceebdc3eb4d3d18d20121038d432d6aa857671ed9b7c67d62b0d2ae3c930e203024978c92a0de8b84142ce58a910000" - peginTxoutProof := "0000002015fa046cecb94e68d91a1c9410d3a220c34a738121b56f17270000000000000036a1af20a2d5bd8c81ddd4b1444f14b28c9c3d049fb2ae6280b0b1ebf19aca64ca4caa660793601934cfc0800e000000044934a86fa650c138bd770a4ca085d35118f433f02f7660ac8b77d4e99120a241cf9ddc148344fd9ed151ecc9b73697ac6d7ac34a2e1ae588f0b39e5b26e39841cd0c149927b444b7cfbb1c9a14f6f7b1e7e578ba2fb55d3999df736fc087e89fd7cd66d686db0ce1fb3de64f6c3dc0518495a4cbc21a7a4dad9f8d7a3b7c380b01b5" - peginClaimScript := "00142c72a18f520851c0da25c3b9a4a7be1daf65a7a3" - peginAmount := uint64(100_000) - liquidAddress := "el1qq2ssn76875d2624p8fmzlm4u959kasmuss0wl4hxdm6hrcz8syruxgx7esshkshs6rdxrrzru7ujw7ne6h3asd46hj3ruv8xh" - - // second pegin - peginTxId2 := "0b387c3b7a8d9fad4d7a1ac2cba4958451c03d6c4fe63dfbe10cdb86d666cdd7" - peginVout2 := uint(2) - peginRawTx2 := "0200000000010159c1a062851325f301282b8ae1124c98228ea7b100eb82e907c6c314001a11860100000000fdffffff03a08601000000000017a914c74490c48b105376e697925c314c47afb00f1303872b1400000000000017a914b5e28484d1f1a5d74878f6ef411d555ac170e62887400d03000000000017a9146ec12f7b07420d693b59c8e6c20b17fbe40503ae87024730440220687049a9caf086d6a2205534acde99e549bde1bb922cfb6f7a7f5204a48ccebc02201c0d84dff34c3ce455fc6806015c160fa35eee86d2fddc7ceebdc3eb4d3d18d20121038d432d6aa857671ed9b7c67d62b0d2ae3c930e203024978c92a0de8b84142ce58a910000" - peginTxoutProof2 := "0000002015fa046cecb94e68d91a1c9410d3a220c34a738121b56f17270000000000000036a1af20a2d5bd8c81ddd4b1444f14b28c9c3d049fb2ae6280b0b1ebf19aca64ca4caa660793601934cfc0800e000000044934a86fa650c138bd770a4ca085d35118f433f02f7660ac8b77d4e99120a241cf9ddc148344fd9ed151ecc9b73697ac6d7ac34a2e1ae588f0b39e5b26e39841cd0c149927b444b7cfbb1c9a14f6f7b1e7e578ba2fb55d3999df736fc087e89fd7cd66d686db0ce1fb3de64f6c3dc0518495a4cbc21a7a4dad9f8d7a3b7c380b01b5" - peginClaimScript2 := "0014e6f7021314806b914a45cce95680b1377f0b7003" - peginAmount2 := uint64(200_000) - liquidAddress2 := "el1qqfun028g4f2nen6a5zj8t20jrsg258k023azkp075rx529g95nf2vysemv6qhkzlntx4gw3tn9ptc0ynr86nqvfaxkar73zzw" - fee := uint64(33) // per pegin! - - psbt, err := liquid.CreateClaimPSBT(peginTxId, - peginVout, - peginRawTx, - peginTxoutProof, - peginClaimScript, - peginAmount-fee, - liquidAddress, - peginTxId2, - peginVout2, - peginRawTx2, - peginTxoutProof2, - peginClaimScript2, - peginAmount2-fee, - liquidAddress2, - fee*2) - if err != nil { - log.Fatalln(err) +// runs after restart, to continue if pegin is ongoing +func loadClaimJoinDB() { + db.Load("ClaimJoin", "PeginHandler", &PeginHandler) + db.Load("ClaimJoin", "ClaimBlockHeight", &ClaimBlockHeight) + db.Load("ClaimJoin", "ClaimStatus", &ClaimStatus) + + db.Load("ClaimJoin", "MyRole", &MyRole) + db.Load("ClaimJoin", "keyToNodeId", &keyToNodeId) + db.Load("ClaimJoin", "claimParties", &claimParties) + + if MyRole == "initiator" { + db.Load("ClaimJoin", "claimPSET", &claimPSET) } - blinded1, complete, err := liquid.ProcessPSBT(psbt, "swaplnd") - if err != nil { - log.Fatalln(err) + var serializedKey []byte + db.Load("ClaimJoin", "serializedPrivateKey", &serializedKey) + myPrivateKey, _ = btcec.PrivKeyFromBytes(serializedKey) +} + +// runs every minute +func OnTimer() { + if !config.Config.PeginClaimJoin || MyRole != "initiator" || len(claimParties) < 2 { + return } - blinded2, complete, err := liquid.ProcessPSBT(blinded1, "swaplnd2") - if err != nil { - log.Fatalln(err) + cl, clean, er := GetClient() + if er != nil { + return } + defer clean() - signed1, complete, err := liquid.ProcessPSBT(blinded2, "swaplnd") - if err != nil { - log.Fatalln(err) + if GetBlockHeight(cl) < ClaimBlockHeight { + // not yet matured + return + } + + startingFee := 36 * len(claimParties) + + if claimPSET == "" { + var err error + claimPSET, err = createClaimPSET(startingFee) + if err != nil { + return + } + db.Save("ClaimJoin", "claimPSET", &claimPSET) } - signed2, complete, err := liquid.ProcessPSBT(signed1, "swaplnd2") + decoded, err := liquid.DecodePSET(claimPSET) if err != nil { - log.Fatalln(err) + return } - hexTx, complete, err := liquid.FinalizePSBT(signed2) + analyzed, err := liquid.AnalyzePSET(claimPSET) if err != nil { - log.Fatalln(err) + return } -*/ -// runs after restart, to continue if pegin is ongoing -func loadClaimJoinDB() { - db.Load("ClaimJoin", "pemToNodeId", &pemToNodeId) - db.Load("ClaimJoin", "PeginHandler", &PeginHandler) - db.Load("ClaimJoin", "ClaimBlockHeight", &ClaimBlockHeight) - db.Load("ClaimJoin", "ClaimStatus", &ClaimStatus) - db.Load("ClaimJoin", "PrivateKey", &myPrivateKey) - db.Load("ClaimJoin", "MyRole", &MyRole) - db.Load("ClaimJoin", "claimParties", &claimParties) + for i, output := range analyzed.Outputs { + if output.Blind && output.Status == "unblinded" { + blinder := decoded.Outputs[i].BlinderIndex + ClaimStatus = "Blinding " + strconv.Itoa(i+1) + "/" + strconv.Itoa(len(claimParties)) + + log.Println(ClaimStatus) + + if blinder == 0 { + // my output + claimPSET, _, err = liquid.ProcessPSET(claimPSET, config.Config.ElementsWallet) + if err != nil { + log.Println("Unable to blind output, cancelling ClaimJoin:", err) + EndClaimJoin("", "Coordination failure") + return + } + } else { + action := "process" + if i == len(claimParties)-1 { + // the final blinder can blind and sign at once + action = "process2" + ClaimStatus = "Signing " + strconv.Itoa(len(claimParties)) + "/" + strconv.Itoa(len(claimParties)) + log.Println(ClaimStatus) + } + + if !SendCoordination(claimParties[blinder].PubKey, &Coordination{ + Action: action, + PSET: claimPSET, + Status: ClaimStatus, + ClaimBlockHeight: ClaimBlockHeight, + }) { + log.Println("Unable to send blind coordination, cancelling ClaimJoin") + EndClaimJoin("", "Coordination failure") + } + return + } + } + } - if MyRole == "joiner" && PeginHandler != "" { - // ask to send again the last transmission - sayAgain = true + // Iterate through inputs in reverse order to sign + for i := len(claimParties) - 1; i >= 0; i-- { + input := decoded.Inputs[i] + if len(input.FinalScriptWitness) == 0 { + ClaimStatus = "Signing " + strconv.Itoa(len(claimParties)-i) + "/" + strconv.Itoa(len(claimParties)) + + log.Println(ClaimStatus) + + if i == 0 { + // my input, last to sign + claimPSET, _, err = liquid.ProcessPSET(claimPSET, config.Config.ElementsWallet) + if err != nil { + log.Println("Unable to sign input, cancelling ClaimJoin:", err) + EndClaimJoin("", "Initiator signing failure") + return + } + } else { + if !SendCoordination(claimParties[i].PubKey, &Coordination{ + Action: "process", + PSET: claimPSET, + Status: ClaimStatus, + ClaimBlockHeight: ClaimBlockHeight, + }) { + log.Println("Unable to send blind coordination, cancelling ClaimJoin") + EndClaimJoin("", "Coordination failure") + } + return + } + } } -} -// runs every minute -func OnTimer() { + if analyzed.Next == "extractor" { + // finalize and check fee + rawHex, done, err := liquid.FinalizePSET(claimPSET) + if err != nil || !done { + log.Println("Unable to finalize PSET, cancelling ClaimJoin:", err) + EndClaimJoin("", "Cannot finalize PSET") + return + } + + decodedTx, err := liquid.DecodeRawTransaction(rawHex) + if err != nil { + log.Println("Cancelling ClaimJoin:", err) + EndClaimJoin("", "Final TX decode failure") + return + } + + exactFee := (decodedTx.DiscountVsize / 10) + 1 + if decodedTx.DiscountVsize%10 == 0 { + exactFee = (decodedTx.DiscountVsize / 10) + } - // + var feeValue int + found := false + + // Iterate over the map + for _, value := range decodedTx.Fee { + feeValue = int(value * 100_000_000) + found = true + break + } - if sayAgain { - if SendCoordination(PeginHandler, &Coordination{Action: "say_again"}) == nil { - sayAgain = false + if !found { + log.Println("Decoded transaction omits fee, cancelling ClaimJoin") + EndClaimJoin("", "Final TX fee failure") + return + } + + if feeValue != exactFee { + + log.Printf("Paid fee: %d, required fee: %d, starting over", feeValue, exactFee) + + // start over with the exact fee + claimPSET, err = createClaimPSET(exactFee) + if err != nil { + return + } + ClaimStatus = "Redo to improve fee" + + db.Save("ClaimJoin", "claimPSET", &claimPSET) + db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) + } else { + // send raw transaction + txId, err := liquid.SendRawTransaction(rawHex) + if err != nil { + if err.Error() == "-27: Transaction already in block chain" { + txId = decodedTx.Txid + } else { + EndClaimJoin("", "Final TX send failure") + return + } + } + EndClaimJoin(txId, "done") } } } // Called when received a broadcast custom message -// Forward the message to all direct peers, unless the source PEM is known already +// Forward the message to all direct peers, unless the source key is known already // (it means the message came back to you from a downstream peer) func Broadcast(fromNodeId string, message *Message) error { - if message.Asset == "pegin_started" && pemToNodeId[message.Sender] != "" || message.Asset == "pegin_ended" && pemToNodeId == nil { + if message.Asset == "pegin_started" && keyToNodeId[message.Sender] != "" || message.Asset == "pegin_ended" && PeginHandler == "" { // has been previously received from upstream return nil } @@ -199,24 +312,55 @@ func Broadcast(fromNodeId string, message *Message) error { } } - if message.Asset == "pegin_started" { + if myNodeId == "" { + // populates myNodeId + if getLndVersion() == 0 { + return fmt.Errorf("Broadcast: Cannot get myNodeId") + } + } + + if fromNodeId == myNodeId { + // do nothing more after sending the original broadcast + return nil + } + + switch message.Asset { + case "pegin_started": + if MyRole == "initiator" { + // two simultaneous initiators conflict, agree if I have no joiners with 50% chance + if len(claimParties) < 2 && mathRand.Intn(2) > 0 { + MyRole = "none" + db.Save("ClaimJoin", "MyRole", MyRole) + } else { + break + } + } // where to forward claimjoin request PeginHandler = message.Sender // store for relaying further encrypted messages - pemToNodeId[message.Sender] = fromNodeId - // currently expected ETA is communicated via Amount + keyToNodeId[message.Sender] = fromNodeId + // Time limit to apply is communicated via Amount ClaimBlockHeight = uint32(message.Amount) - ClaimStatus = "Pegin started, awaiting joiners" - } else if message.Asset == "pegin_ended" { - PeginHandler = "" - // delete the routing map - pemToNodeId = nil - ClaimBlockHeight = 0 - ClaimStatus = "No third-party pegin is pending" + ClaimStatus = "🧬 Received invitation to ClaimJoin" + log.Println(ClaimStatus) + case "pegin_ended": + // only trust the message from the original handler + if PeginHandler == message.Sender { + txId := message.Payload + if txId != "" { + ClaimStatus = "ClaimJoin pegin successful! Liquid TxId: " + txId + } else { + ClaimStatus = "Invitation to ClaimJoin revoked" + } + log.Println(ClaimStatus) + resetClaimJoin() + return nil + } } // persist to db - db.Save("ClaimJoin", "pemToNodeId", pemToNodeId) + db.Save("ClaimJoin", "MyRole", MyRole) + db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) db.Save("ClaimJoin", "PeginHandler", PeginHandler) db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) @@ -224,164 +368,272 @@ func Broadcast(fromNodeId string, message *Message) error { return nil } -// Encrypt and send message to an anonymous peer identified only by public key in PEM format -// New keys are generated at the start of each pegin session +// Encrypt and send message to an anonymous peer identified by base64 public key +// New keys are generated at the start of each ClaimJoin session // Peers track sources of encrypted messages to forward back the replies -func SendCoordination(destinationPEM string, message *Coordination) error { +func SendCoordination(destinationPubKey string, message *Coordination) bool { payload, err := json.Marshal(message) if err != nil { - return err + return false } - destinationNodeId := pemToNodeId[destinationPEM] + destinationNodeId := keyToNodeId[destinationPubKey] if destinationNodeId == "" { - log.Println("Cannot send coordination, destination PEM has no matching NodeId") - return fmt.Errorf("destination PEM has no matching NodeId") - } - - // Deserialize the received PEM string back to a public key - deserializedPublicKey, err := pemToPublicKey(destinationPEM) - if err != nil { - log.Println("Error deserializing public key:", err) - return err + log.Println("Cannot send coordination, destination PubKey has no matching NodeId") + return false } - // Encrypt the message using the deserialized receiver's public key - ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, deserializedPublicKey, payload, nil) + // Encrypt the message using the base64 receiver's public key + ciphertext, err := eciesEncrypt(destinationPubKey, payload) if err != nil { log.Println("Error encrypting message:", err) - return err + return false } cl, clean, er := GetClient() if er != nil { - return er + return false } defer clean() return SendCustomMessage(cl, destinationNodeId, &Message{ Version: MessageVersion, Memo: "process", - Sender: myPublicPEM, - Destination: destinationPEM, + Sender: myPublicKey(), + Destination: destinationPubKey, Payload: ciphertext, - }) + }) == nil } -// Either forward to final distination or decrypt and process -func Process(message *Message) error { +// Either forward to final destination or decrypt and process +func Process(message *Message, senderNodeId string) { + + if !config.Config.PeginClaimJoin { + // spam + return + } cl, clean, er := GetClient() if er != nil { - return er + return } defer clean() - if message.Destination == myPublicPEM { + if keyToNodeId[message.Sender] != senderNodeId { + // save source key map + keyToNodeId[message.Sender] = senderNodeId + // persist to db + db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) + } + + log.Println("My pubKey:", myPublicKey()) + + if message.Destination == myPublicKey() { + // Convert to []byte + payload, err := base64.StdEncoding.DecodeString(message.Payload) + if err != nil { + log.Printf("Error decoding payload: %s", err) + return + } + // Decrypt the message using my private key - plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, myPrivateKey, message.Payload, nil) + plaintext, err := eciesDecrypt(myPrivateKey, payload) if err != nil { - return fmt.Errorf("error decrypting message: %s", err) + log.Printf("Error decrypting payload: %s", err) + return } // recover the struct var msg Coordination err = json.Unmarshal(plaintext, &msg) if err != nil { - return fmt.Errorf("received an incorrectly formed message: %s", err) - + log.Printf("Received an incorrectly formed Coordination: %s", err) + return } - log.Println(msg) - - if msg.ClaimBlockHeight > ClaimBlockHeight { - ClaimBlockHeight = msg.ClaimBlockHeight - } + claimPSET = msg.PSET switch msg.Action { case "add": if MyRole != "initiator" { - return fmt.Errorf("cannot add a joiner, not a claim initiator") + log.Printf("Cannot add a joiner, not a claim initiator") + return + } + + if ok, status := addClaimParty(&msg.Joiner); ok { + ClaimBlockHeight = max(ClaimBlockHeight, msg.ClaimBlockHeight) + + if SendCoordination(msg.Joiner.PubKey, &Coordination{ + Action: "confirm_add", + ClaimBlockHeight: ClaimBlockHeight, + Status: status, + }) { + ClaimStatus = "Added new joiner, total participants: " + strconv.Itoa(len(claimParties)) + log.Println(ClaimStatus) + } + } else { + if SendCoordination(msg.Joiner.PubKey, &Coordination{ + Action: "refuse_add", + Status: status, + }) { + log.Println("Refused new joiner: ", status) + } } case "remove": - case "process": // blind or sign - case "process2": // process twice to blind and sign + if MyRole != "initiator" { + log.Printf("Cannot remove a joiner, not a claim initiator") + return + } - } + if removeClaimParty(msg.Joiner.PubKey) { + ClaimStatus = "Removed a joiner, total participants: " + strconv.Itoa(len(claimParties)) + // persist to db + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + log.Println(ClaimStatus) + // erase PSET to start over + claimPSET = "" + // persist to db + db.Save("ClaimJoin", "claimPSET", claimPSET) + } else { + log.Println("Cannot remove joiner, not in the list") + } - return nil - } + case "confirm_add": + ClaimBlockHeight = msg.ClaimBlockHeight + MyRole = "joiner" + ClaimStatus = msg.Status + log.Println(ClaimStatus) - // relay further - destinationNodeId := pemToNodeId[message.Destination] - if destinationNodeId == "" { - return fmt.Errorf("cannot relay, destination PEM has no matching NodeId") - } - return SendCustomMessage(cl, destinationNodeId, message) + // persist to db + db.Save("ClaimJoin", "MyRole", MyRole) + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) -} + case "refuse_add": + log.Println(msg.Status) + // forget pegin handler, so that cannot initiate new ClaimJoin + PeginHandler = "" + ClaimStatus = msg.Status + MyRole = "none" -// generates new message keys -func GenerateKeys() error { - // Generate RSA key pair - var err error + // persist to db + db.Save("ClaimJoin", "MyRole", MyRole) + db.Save("ClaimJoin", "PeginHandler", PeginHandler) + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - myPrivateKey, err = rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - log.Println("Error generating my private key:", err) - return err - } + case "process2": // process twice to blind and sign + if MyRole != "joiner" { + log.Println("received process2 while not being a joiner") + return + } - myPublicPEM, err = publicKeyToPEM(&myPrivateKey.PublicKey) - if err != nil { - log.Println("Error obtaining my public PEM:", err) - return err - } + // process my output + claimPSET, _, err = liquid.ProcessPSET(claimPSET, config.Config.ElementsWallet) + if err != nil { + log.Println("Unable to process PSET:", err) + return + } + fallthrough // continue to second pass - // persist to db - db.Save("ClaimJoin", "PrivateKey", myPrivateKey) + case "process": // blind or sign + if !verifyPSET() { + log.Println("PSET verification failure!") + if MyRole == "initiator" { + // kick the joiner who returned broken PSET + if ok := removeClaimParty(message.Sender); ok { + ClaimStatus = "Joiner " + message.Sender + " kicked, total participants: " + strconv.Itoa(len(claimParties)) + // persist to db + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + log.Println(ClaimStatus) + // erase PSET to start over + claimPSET = "" + // persist to db + db.Save("ClaimJoin", "claimPSET", claimPSET) + + SendCoordination(message.Sender, &Coordination{ + Action: "refuse_add", + Status: "Kicked for broken PSET return", + }) + } else { + EndClaimJoin("", "Coordination failure") + } + return + } else { + // remove yourself from ClaimJoin + if SendCoordination(PeginHandler, &Coordination{ + Action: "remove", + Joiner: *claimParties[0], + }) { + ClaimBlockHeight = 0 + // forget pegin handler, so that cannot initiate new ClaimJoin + PeginHandler = "" + ClaimStatus = "Kicked from ClaimJoin" + log.Println(ClaimStatus) + + db.Save("ClaimJoin", "PeginHandler", &PeginHandler) + db.Save("ClaimJoin", "PeginHandler", &PeginHandler) + + // disable ClaimJoin + config.Config.PeginClaimJoin = false + config.Save() + } + } + return + } - return nil -} + if MyRole == "initiator" { + // Save received claimPSET, execute OnTimer + db.Save("ClaimJoin", "claimPSET", &claimPSET) + OnTimer() + return + } -// Convert a public key to PEM format -func publicKeyToPEM(pub *rsa.PublicKey) (string, error) { - pubASN1, err := x509.MarshalPKIXPublicKey(pub) - if err != nil { - return "", err - } + if MyRole != "joiner" { + log.Println("received 'process' unexpected") + return + } - pubPEM := pem.EncodeToMemory(&pem.Block{ - Type: "RSA PUBLIC KEY", - Bytes: pubASN1, - }) + // process my output + claimPSET, _, err = liquid.ProcessPSET(claimPSET, config.Config.ElementsWallet) + if err != nil { + log.Println("Unable to process PSET:", err) + return + } - return string(pubPEM), nil -} + ClaimBlockHeight = msg.ClaimBlockHeight + ClaimStatus = msg.Status -// Convert a PEM-formatted string to a public key -func pemToPublicKey(pubPEM string) (*rsa.PublicKey, error) { - block, _ := pem.Decode([]byte(pubPEM)) - if block == nil { - return nil, fmt.Errorf("failed to decode PEM block") + log.Println(ClaimStatus) + + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + + // return PSET to Handler + if !SendCoordination(PeginHandler, &Coordination{ + Action: "process", + PSET: claimPSET, + }) { + log.Println("Unable to send blind coordination, cancelling ClaimJoin") + EndClaimJoin("", "Coordination failure") + } + } } - pub, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return nil, err + // message not to me, relay further + destinationNodeId := keyToNodeId[message.Destination] + if destinationNodeId == "" { + log.Println("Cannot relay: destination PubKey " + message.Destination + " has no matching NodeId") + return } - switch pub := pub.(type) { - case *rsa.PublicKey: - return pub, nil - default: - return nil, fmt.Errorf("not an RSA public key") + err := SendCustomMessage(cl, destinationNodeId, message) + if err != nil { + log.Println("Cannot relay:", err) } } -// called by claim join initiator after his pegin funding tx confirms +// called for claim join initiator after his pegin funding tx confirms func InitiateClaimJoin(claimBlockHeight uint32) bool { if myNodeId == "" { // populates myNodeId @@ -390,19 +642,31 @@ func InitiateClaimJoin(claimBlockHeight uint32) bool { } } + myPrivateKey = generatePrivateKey() + if myPrivateKey != nil { + // persist to db + savePrivateKey() + } else { + return false + } + if Broadcast(myNodeId, &Message{ Version: MessageVersion, Memo: "broadcast", Asset: "pegin_started", Amount: uint64(claimBlockHeight), + Sender: myPublicKey(), }) == nil { ClaimBlockHeight = claimBlockHeight party := createClaimParty(claimBlockHeight) if party != nil { // initiate array of claim parties claimParties = nil - claimParties[0] = party + claimParties = append(claimParties, party) + ClaimStatus = "Invites sent, awaiting joiners" // persist to db + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) db.Save("ClaimJoin", "claimParties", claimParties) return true } @@ -410,7 +674,59 @@ func InitiateClaimJoin(claimBlockHeight uint32) bool { return false } -// called by ClaimJoin joiner candidate after his pegin funding tx confirms +// called by claim join initiator after posting claim tx or on error +func EndClaimJoin(txId string, status string) bool { + if myNodeId == "" { + // populates myNodeId + if getLndVersion() == 0 { + return false + } + } + + if Broadcast(myNodeId, &Message{ + Version: MessageVersion, + Memo: "broadcast", + Asset: "pegin_ended", + Amount: uint64(0), + Sender: myPublicKey(), + Payload: txId, + Destination: status, + }) == nil { + if txId != "" { + log.Println("ClaimJoin pegin success! Liquid TxId:", txId) + // signal to telegram bot + config.Config.PeginTxId = txId + config.Config.PeginClaimScript = "done" + } else { + log.Println("ClaimJoin failed") + // signal to telegram bot + config.Config.PeginClaimScript = "failed" + } + resetClaimJoin() + return true + } + return false +} + +func resetClaimJoin() { + // eraze all traces + ClaimBlockHeight = 0 + claimParties = nil + MyRole = "none" + PeginHandler = "" + ClaimStatus = "No ClaimJoin peg-in is pending" + keyToNodeId = make(map[string]string) + + // persist to db + db.Save("ClaimJoin", "claimParties", claimParties) + db.Save("ClaimJoin", "PeginHandler", PeginHandler) + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + db.Save("ClaimJoin", "MyRole", MyRole) + db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) +} + +// called for ClaimJoin joiner candidate after his pegin funding tx confirms func JoinClaimJoin(claimBlockHeight uint32) bool { if myNodeId == "" { // populates myNodeId @@ -419,13 +735,28 @@ func JoinClaimJoin(claimBlockHeight uint32) bool { } } + myPrivateKey = generatePrivateKey() + if myPrivateKey != nil { + // persist to db + savePrivateKey() + } else { + return false + } + party := createClaimParty(claimBlockHeight) if party != nil { if SendCoordination(PeginHandler, &Coordination{ Action: "add", Joiner: *party, ClaimBlockHeight: claimBlockHeight, - }) == nil { + }) { + // initiate array of claim parties for single entry + claimParties = nil + claimParties = append(claimParties, party) + ClaimStatus = "Responded to invitation, awaiting confirmation" + // persist to db + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + db.Save("ClaimJoin", "claimParties", claimParties) ClaimBlockHeight = claimBlockHeight return true } @@ -472,6 +803,299 @@ func createClaimParty(claimBlockHeight uint32) *ClaimParty { return nil } party.Address = res.Address + party.PubKey = myPublicKey() return party } + +// add claim party to the list +func addClaimParty(newParty *ClaimParty) (bool, string) { + for _, party := range claimParties { + if party.ClaimScript == newParty.ClaimScript { + // is already in the list + return true, "Was already in the list, total participants: " + strconv.Itoa(len(claimParties)) + } + } + + if len(claimParties) == maxParties { + return false, "Refused to join, over limit of " + strconv.Itoa(maxParties) + } + + // verify claimBlockHeight + proof, err := bitcoin.GetTxOutProof(config.Config.PeginTxId) + if err != nil { + return false, "Refused to join, TX not confirmed" + } + + if proof != newParty.TxoutProof { + log.Printf("New joiner's TxoutProof was wrong") + newParty.TxoutProof = proof + } + + txHeight := uint32(internet.GetTxHeight(newParty.TxId)) + + if txHeight > 0 && txHeight+PeginBlocks != newParty.ClaimBlockHeight { + log.Printf("New joiner's ClaimBlockHeight was wrong") + newParty.ClaimBlockHeight = txHeight + PeginBlocks + } + + claimParties = append(claimParties, newParty) + + // persist to db + db.Save("ClaimJoin", "claimParties", claimParties) + + return true, "Successfully joined, total participants: " + strconv.Itoa(len(claimParties)) +} + +// remove claim party from the list by public key +func removeClaimParty(pubKey string) bool { + var newClaimParties []*ClaimParty + found := false + claimBlockHeight := uint32(0) + + for _, party := range claimParties { + if party.PubKey == pubKey { + found = true + } else { + newClaimParties = append(newClaimParties, party) + claimBlockHeight = max(claimBlockHeight, party.ClaimBlockHeight) + } + } + + if !found { + return false + } + + if claimBlockHeight < ClaimBlockHeight { + ClaimBlockHeight = claimBlockHeight + // persist to db + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + } + + claimParties = newClaimParties + + // persist to db + db.Save("ClaimJoin", "claimParties", claimParties) + + return true +} + +func generatePrivateKey() *btcec.PrivateKey { + privKey, err := btcec.NewPrivateKey() + + if err != nil { + log.Println("Error generating private key") + return nil + } + + return privKey +} + +func myPublicKey() string { + return publicKeyToBase64(myPrivateKey.PubKey()) +} + +/* +// Payload is encrypted with a new symmetric AES key and returned as base64 string +// The AES key is encrypted using destination secp256k1 pubKey, returned as base64 +func encryptPayload(pubKeyString string, plaintext []byte) (string, string, error) { + // Generate AES key + aesKey := make([]byte, 32) // 256-bit key + if _, err := rand.Read(aesKey); err != nil { + return "", "", err + } + + block, err := aes.NewCipher(aesKey) + if err != nil { + return "", "", err + } + + nonce := make([]byte, aes.BlockSize) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", "", err + } + + stream := cipher.NewCTR(block, nonce) + ciphertext := make([]byte, len(plaintext)) + stream.XORKeyStream(ciphertext, plaintext) + + // Prepend the nonce to the ciphertext + finalCiphertext := append(nonce, ciphertext...) + + // convert to base64 + base64cypherText := base64.StdEncoding.EncodeToString(finalCiphertext) + + // Encrypt the AES key using RSA public key and returns the encrypted key in base64 + encryptedKey, err := eciesEncrypt(pubKeyString, aesKey) + if err != nil { + return "", "", err + } + + return base64cypherText, encryptedKey, nil +} + +*/ + +// Encrypt with base64 public key +func eciesEncrypt(pubKeyString string, message []byte) (string, error) { + + pubKey, err := base64ToPublicKey(pubKeyString) + if err != nil { + return "", err + } + + ephemeralPrivKey := generatePrivateKey() + sharedSecret := sha256.Sum256(btcec.GenerateSharedSecret(ephemeralPrivKey, pubKey)) + + hkdf := hkdf.New(sha256.New, sharedSecret[:], nil, nil) + encryptionKey := make([]byte, chacha20poly1305.KeySize) + if _, err := io.ReadFull(hkdf, encryptionKey); err != nil { + return "", err + } + + aead, err := chacha20poly1305.New(encryptionKey) + if err != nil { + return "", err + } + + nonce := make([]byte, chacha20poly1305.NonceSize) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", err + } + + ciphertext := aead.Seal(nil, nonce, message, nil) + + result := append(ephemeralPrivKey.PubKey().SerializeCompressed(), nonce...) + result = append(result, ciphertext...) + + return base64.StdEncoding.EncodeToString(result), nil +} + +func eciesDecrypt(privKey *btcec.PrivateKey, ciphertext []byte) ([]byte, error) { + ephemeralPubKey, err := btcec.ParsePubKey(ciphertext[:33]) + if err != nil { + return nil, err + } + + nonce := ciphertext[33 : 33+chacha20poly1305.NonceSize] + encryptedMessage := ciphertext[33+chacha20poly1305.NonceSize:] + + sharedSecret := sha256.Sum256(btcec.GenerateSharedSecret(privKey, ephemeralPubKey)) + + hkdf := hkdf.New(sha256.New, sharedSecret[:], nil, nil) + encryptionKey := make([]byte, chacha20poly1305.KeySize) + if _, err := io.ReadFull(hkdf, encryptionKey); err != nil { + return nil, err + } + + aead, err := chacha20poly1305.New(encryptionKey) + if err != nil { + return nil, err + } + + decryptedMessage, err := aead.Open(nil, nonce, encryptedMessage, nil) + if err != nil { + return nil, err + } + + return decryptedMessage, nil +} + +func publicKeyToBase64(pubKey *btcec.PublicKey) string { + pubKeyBytes := pubKey.SerializeCompressed() // Compressed format + return base64.StdEncoding.EncodeToString(pubKeyBytes) +} + +func base64ToPublicKey(pubKeyStr string) (*btcec.PublicKey, error) { + pubKeyBytes, err := base64.StdEncoding.DecodeString(pubKeyStr) + if err != nil { + return nil, err + } + pubKey, err := btcec.ParsePubKey(pubKeyBytes) + if err != nil { + return nil, err + } + return pubKey, nil +} + +func createClaimPSET(totalFee int) (string, error) { + // Create the inputs array + var inputs []map[string]interface{} + + // Create the outputs array + var outputs []map[string]interface{} + + feeToSplit := totalFee + feePart := totalFee / len(claimParties) + + // fill in the arrays + for i, party := range claimParties { + input := map[string]interface{}{ + "txid": party.TxId, + "vout": party.Vout, + "pegin_bitcoin_tx": party.RawTx, + "pegin_txout_proof": party.TxoutProof, + "pegin_claim_script": party.ClaimScript, + } + + if i == len(claimParties)-1 { + // the last joiner pays higher fee if it cannot be divided equally + feePart = feeToSplit + } + + output := map[string]interface{}{ + party.Address: liquid.ToBitcoin(party.Amount - uint64(feePart)), + "blinder_index": i, + } + + feeToSplit -= feePart + inputs = append(inputs, input) + outputs = append(outputs, output) + } + + // shuffle the outputs + for i := len(outputs) - 1; i > 0; i-- { + j := mathRand.Intn(i + 1) + outputs[i], outputs[j] = outputs[j], outputs[i] + } + + // add total fee output + outputs = append(outputs, map[string]interface{}{ + "fee": liquid.ToBitcoin(uint64(totalFee)), + }) + + // add op_return + outputs = append(outputs, map[string]interface{}{ + "data": "6a0f506565725377617020576562205549", + }) + + // Combine inputs and outputs into the parameters array + params := []interface{}{inputs, outputs} + + return liquid.CreatePSET(params) +} + +// Serialize btcec.PrivateKey and save to db +func savePrivateKey() { + if myPrivateKey == nil { + return + } + data := myPrivateKey.Serialize() + db.Save("ClaimJoin", "serializedPrivateKey", data) +} + +// checks that the output includes my address and amount +func verifyPSET() bool { + decoded, err := liquid.DecodePSET(claimPSET) + if err != nil { + return false + } + + for _, output := range decoded.Outputs { + // 50 sats maximum fee allowed + if output.Script.Address == claimParties[0].Address && liquid.ToBitcoin(claimParties[0].Amount)-output.Amount < 0.0000005 { + return true + } + } + return false +} diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index b79f9e0..5d9d2f6 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -1318,6 +1318,10 @@ func InitiateClaimJoin(claimHeight uint32) bool { return false } func JoinClaimJoin(claimHeight uint32) bool { return false } var ( - PeginHandler = "" - MyRole = "none" + PeginHandler = "" + MyRole = "none" + ClaimStatus = "" + ClaimBlockHeight = uint32(0) ) + +const PeginBlocks = 102 diff --git a/cmd/psweb/ln/common.go b/cmd/psweb/ln/common.go index f98419a..b3f578d 100644 --- a/cmd/psweb/ln/common.go +++ b/cmd/psweb/ln/common.go @@ -154,7 +154,7 @@ type Message struct { // for encrypted communications via peer relay Sender string `json:"sender"` Destination string `json:"destination"` - Payload []byte `json:"payload"` + Payload string `json:"payload"` } type BalanceInfo struct { @@ -291,6 +291,9 @@ func AutoFeeRatesSummary(channelId uint64) (string, bool) { } func LoadDB() { + // load ClaimJoin variables + loadClaimJoinDB() + // load rebates from db db.Load("Swaps", "SwapRebates", &SwapRebates) @@ -321,9 +324,6 @@ func LoadDB() { return } } - - // init ClaimJoin - loadClaimJoinDB() } func calculateAutoFee(channelId uint64, params *AutoFeeParams, liqPct int, oldFee int) int { diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index f85b25c..760deef 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -1209,22 +1209,20 @@ func subscribeMessages(ctx context.Context, client lnrpc.LightningClient) error } // messages related to pegin claim join - if msg.Memo == "process" { - err = Process(&msg) - if err != nil { - log.Println(err) - } + if msg.Memo == "process" && config.Config.PeginClaimJoin { + Process(&msg, nodeId) } // received request for information if msg.Memo == "poll" { - if MyRole == "initiator" { + if MyRole == "initiator" && myPublicKey() != "" && len(claimParties) < maxParties { // repeat pegin start broadcast SendCustomMessage(client, nodeId, &Message{ Version: MessageVersion, Memo: "broadcast", Asset: "pegin_started", Amount: uint64(ClaimBlockHeight), + Sender: myPublicKey(), }) } @@ -1294,6 +1292,8 @@ func SendCustomMessage(client lnrpc.LightningClient, peerId string, message *Mes return err } + log.Println("Message size:", len(data)) + req := &lnrpc.SendCustomMessageRequest{ Peer: peerByte, Type: messageType, diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index d76a796..6bd99ec 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -348,9 +348,6 @@ func onTimer(firstRun bool) { // Back up to Telegram if Liquid balance changed liquidBackup(false) - // Check if pegin can be claimed - checkPegin() - // Handle ClaimJoin go ln.OnTimer() @@ -383,6 +380,8 @@ func onTimer(firstRun bool) { if !firstRun { // skip first run so that forwards have time to download go ln.ApplyAutoFees() + // Check if pegin can be claimed or joined + go checkPegin() } // advertise Liquid balance @@ -1093,17 +1092,56 @@ func checkPegin() { return } - if ln.MyRole != "none" { - // claim is handled by ClaimJoin - return - } - cl, clean, er := ln.GetClient() if er != nil { return } defer clean() + currentBlockHeight := ln.GetBlockHeight(cl) + + if ln.MyRole == "none" && currentBlockHeight > ln.ClaimBlockHeight { + // invitation expired + ln.PeginHandler = "" + db.Save("ClaimJoin", "PeginHandler", ln.PeginHandler) + } + + if config.Config.PeginClaimJoin { + if ln.MyRole != "none" { + if currentBlockHeight > ln.ClaimBlockHeight+10 { + // something is wrong, claim pegin individually + t := "🧬 ClaimJoin matured 10 blocks ago, switching to individual claim" + log.Println(t) + telegramSendMessage(t) + ln.MyRole = "none" + config.Config.PeginClaimJoin = false + config.Save() + ln.EndClaimJoin("", "Something went wrong") + } + return + } + + t := "" + switch config.Config.PeginClaimScript { + case "done": + t = "🧬 ClaimJoin pegin successfull! Liquid TxId: `" + config.Config.PeginTxId + "`" + case "failed": + t = "🧬 ClaimJoin pegin failed: " + config.Config.PeginTxId + default: + goto singlePegin + } + + // finish by sending telegram message + telegramSendMessage(t) + config.Config.PeginClaimScript = "" + config.Config.PeginTxId = "" + config.Config.PeginClaimJoin = false + config.Save() + return + } + +singlePegin: + confs, _ := ln.GetTxConfirmations(cl, config.Config.PeginTxId) if confs < 0 && config.Config.PeginReplacedTxId != "" { confs, _ = ln.GetTxConfirmations(cl, config.Config.PeginReplacedTxId) @@ -1119,7 +1157,7 @@ func checkPegin() { if config.Config.PeginClaimScript == "" { log.Println("BTC withdrawal complete, txId: " + config.Config.PeginTxId) telegramSendMessage("BTC withdrawal complete. TxId: `" + config.Config.PeginTxId + "`") - } else if confs > 101 && ln.MyRole == "none" { + } else if confs >= ln.PeginBlocks && ln.MyRole == "none" { // claim individual pegin failed := false proof := "" @@ -1130,7 +1168,7 @@ func checkPegin() { if err == nil { txid, err = liquid.ClaimPegin(rawTx, proof, config.Config.PeginClaimScript) // claimpegin takes long time, allow it to timeout - if err != nil && err.Error() != "timeout reading data from server" { + if err != nil && err.Error() != "timeout reading data from server" && err.Error() != "-4: Error: The transaction was rejected! Reason given: pegin-already-claimed" { failed = true } } else { @@ -1148,27 +1186,38 @@ func checkPegin() { log.Println("Claim Script:", config.Config.PeginClaimScript) telegramSendMessage("❗ Peg-in claim FAILED! See log for details.") } else { - log.Println("Peg-in success! Liquid TxId:", txid) - telegramSendMessage("💸 Peg-in success! Liquid TxId: `" + txid + "`") + log.Println("Peg-in successful! Liquid TxId:", txid) + telegramSendMessage("💸 Peg-in successfull! Liquid TxId: `" + txid + "`") } } else { if config.Config.PeginClaimJoin { if ln.MyRole == "none" { - currentBlockHeight := ln.GetBlockHeight(cl) - claimHeight := currentBlockHeight + 102 - uint32(confs) + claimHeight := currentBlockHeight + ln.PeginBlocks - uint32(confs) if ln.PeginHandler == "" { - // I become pegin handler + // I will coordinate this handler if ln.InitiateClaimJoin(claimHeight) { + t := "🧬 Sent ClaimJoin invitations" + log.Println(t) + telegramSendMessage(t) + ln.MyRole = "initiator" // persist to db db.Save("ClaimJoin", "MyRole", ln.MyRole) + } else { + log.Println("Failed to initiate ClaimJoin, continuing as a single pegin") + config.Config.PeginClaimJoin = false + config.Save() } - } else { + } else if currentBlockHeight < ln.ClaimBlockHeight { // join by replying to initiator if ln.JoinClaimJoin(claimHeight) { - ln.MyRole = "joiner" - // persist to db - db.Save("ClaimJoin", "MyRole", ln.MyRole) + t := "🧬 Applied to ClaimJoin group" + log.Println(t) + telegramSendMessage(t) + } else { + log.Println("Failed to apply to ClaimJoin, continuing as a single pegin") + config.Config.PeginClaimJoin = false + config.Save() } } } diff --git a/cmd/psweb/telegram.go b/cmd/psweb/telegram.go index 1fdf245..605322e 100644 --- a/cmd/psweb/telegram.go +++ b/cmd/psweb/telegram.go @@ -95,15 +95,34 @@ func telegramStart() { t = "❗ Error: " + er.Error() } else { confs, _ := ln.GetTxConfirmations(cl, config.Config.PeginTxId) - duration := time.Duration(10*(102-confs)) * time.Minute - formattedDuration := time.Time{}.Add(duration).Format("15h 04m") - t = "⏰ Amount: " + formatWithThousandSeparators(uint64(config.Config.PeginAmount)) + " sats, Confs: " + strconv.Itoa(int(confs)) - if config.Config.PeginClaimScript != "" { - t += "/102, Time left: " + formattedDuration + if config.Config.PeginClaimJoin { + bh := ln.GetBlockHeight(cl) + duration := time.Duration(10*(ln.ClaimBlockHeight-bh)) * time.Minute + if ln.ClaimBlockHeight == 0 { + duration = time.Duration(10*(ln.PeginBlocks-confs)) * time.Minute + } + formattedDuration := time.Time{}.Add(duration).Format("15h 04m") + if duration < 0 { + formattedDuration = "Past due!" + } + t = ln.ClaimStatus + if ln.MyRole == "none" && ln.PeginHandler != "" { + t += ". Time left to apply: " + formattedDuration + } else if confs > 0 { + t += ". Claim ETA: " + formattedDuration + } + } else { + // sole pegin + duration := time.Duration(10*(ln.PeginBlocks-confs)) * time.Minute + formattedDuration := time.Time{}.Add(duration).Format("15h 04m") + t = "⏰ Amount: " + formatWithThousandSeparators(uint64(config.Config.PeginAmount)) + " sats, Confs: " + strconv.Itoa(int(confs)) + if config.Config.PeginClaimScript != "" { + t += "/102, Time left: " + formattedDuration + } + t += ". TxId: `" + config.Config.PeginTxId + "`" } - t += ". TxId: `" + config.Config.PeginTxId + "`" - clean() } + clean() } telegramSendMessage(t) case "/autoswaps": diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index 9ae0b28..714158a 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -67,19 +67,19 @@
- {{if .HasDiscountedvSize}} -
+ {{if .CanClaimJoin}} +
- ClaimJoin + 🧬 ClaimJoin
@@ -136,12 +136,12 @@ {{.Confirmations}} {{if .IsPegin}} - / 102 + / {{.TargetConfirmations}} - T left: + ETA: {{.Duration}} @@ -156,6 +156,16 @@ {{.PeginTxId}} + {{if .IsClaimJoin}} + + + CJ: + + + {{.ClaimJoinStatus}} + + + {{end}} {{if .CanBump}} @@ -282,6 +292,9 @@ document.getElementById('sendAddress').removeAttribute('required'); document.getElementById("sendButton").value = "Start Peg-In"; document.getElementById("isPegin").value = "true"; + {{if .CanClaimJoin}} + document.getElementById("claimJoinField").style.display = ""; + {{end}} calculateTransactionFee(); } @@ -292,6 +305,9 @@ document.getElementById('sendAddress').setAttribute('required', 'true'); document.getElementById("sendButton").value = "Send Bitcoin"; document.getElementById("isPegin").value = "false"; + {{if .CanClaimJoin}} + document.getElementById("claimJoinField").style.display = "none"; + {{end}} calculateTransactionFee(); } @@ -628,9 +644,9 @@ //liquid peg-in fee estimate let liquidFee = "45"; fee += 45; - {{if .HasDiscountedvSize}} + {{if .CanClaimJoin}} if (document.getElementById("claimJoin").checked) { - liquidFee = "32-45" + liquidFee = "35-45" } {{end}} text += "Liquid chain fee: " + liquidFee + " sats\n"; @@ -648,7 +664,7 @@ text += "Cost PPM: " + formatWithThousandSeparators(Math.round(fee * 1000000 / netAmount)); let hours = "17 hours"; - {{if .HasDiscountedvSize}} + {{if .CanClaimJoin}} if (document.getElementById("claimJoin").checked) { hours = "17-34 hours"; } diff --git a/go.mod b/go.mod index 1d5d1bd..d142dbd 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,8 @@ replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-d require ( github.com/alexmullins/zip v0.0.0-20180717182244-4affb64b04d0 - github.com/btcsuite/btcd v0.24.2-beta.rc1.0.20240403021926-ae5533602c46 + github.com/btcsuite/btcd v0.24.2 + github.com/btcsuite/btcd/btcec/v2 v2.3.3 github.com/btcsuite/btcd/btcutil v1.1.5 github.com/btcsuite/btcd/btcutil/psbt v1.1.8 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 @@ -21,8 +22,9 @@ require ( github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/gorilla/mux v1.8.1 github.com/gorilla/sessions v1.2.2 - github.com/lightningnetwork/lnd v0.18.0-beta + github.com/lightningnetwork/lnd v0.18.2-beta go.etcd.io/bbolt v1.3.10 + golang.org/x/crypto v0.22.0 golang.org/x/net v0.24.0 google.golang.org/grpc v1.63.2 gopkg.in/macaroon.v2 v2.1.0 @@ -36,9 +38,8 @@ require ( github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/siphash v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.3.3 // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect - github.com/btcsuite/btcwallet v0.16.10-0.20240404104514-b2f31f9045fb // indirect + github.com/btcsuite/btcwallet v0.16.10-0.20240706055350-e391a1c31df2 // indirect github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4 // indirect github.com/btcsuite/btcwallet/wallet/txrules v1.2.1 // indirect github.com/btcsuite/btcwallet/wallet/txsizes v1.2.4 // indirect @@ -170,7 +171,6 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.23.0 // indirect - golang.org/x/crypto v0.22.0 // indirect golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/sys v0.19.0 // indirect diff --git a/go.sum b/go.sum index 74fe8cc..9de3642 100644 --- a/go.sum +++ b/go.sum @@ -69,8 +69,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= -github.com/btcsuite/btcd v0.24.2-beta.rc1.0.20240403021926-ae5533602c46 h1:tjpNTdZNQqE14menwDGAxWfzN0DFHVTXFEyEL8yvA/4= -github.com/btcsuite/btcd v0.24.2-beta.rc1.0.20240403021926-ae5533602c46/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= +github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= +github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0= @@ -88,8 +88,8 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtyd github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/btcwallet v0.16.10-0.20240404104514-b2f31f9045fb h1:qoIOlBPRZWtfpcbQlNFf67Wz8ZlXo+mxQc9Pnbm/iqU= -github.com/btcsuite/btcwallet v0.16.10-0.20240404104514-b2f31f9045fb/go.mod h1:2C3Q/MhYAKmk7F+Tey6LfKtKRTdQsrCf8AAAzzDPmH4= +github.com/btcsuite/btcwallet v0.16.10-0.20240706055350-e391a1c31df2 h1:mJquwdcEA4hZip4XKbRPAM9rOrus6wlNEcWzMz5CHsI= +github.com/btcsuite/btcwallet v0.16.10-0.20240706055350-e391a1c31df2/go.mod h1:SLFUSQbP8ON/wxholYMfVLvGPJyk7boczOW/ob+nww4= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4 h1:poyHFf7+5+RdxNp5r2T6IBRD7RyraUsYARYbp/7t4D8= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4/go.mod h1:GETGDQuyq+VFfH1S/+/7slLM/9aNa4l7P4ejX6dJfb0= github.com/btcsuite/btcwallet/wallet/txrules v1.2.1 h1:UZo7YRzdHbwhK7Rhv3PO9bXgTxiOH45edK5qdsdiatk= @@ -459,8 +459,8 @@ github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display h1:pRdza2wl github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f h1:Pua7+5TcFEJXIIZ1I2YAUapmbcttmLj4TTi786bIi3s= github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= -github.com/lightningnetwork/lnd v0.18.0-beta h1:3cH7npkUh156FI5kb6bZbiO+Fl3YD+Bu2UbFKoLZ4lo= -github.com/lightningnetwork/lnd v0.18.0-beta/go.mod h1:1SA9iv9rZddNAcfP38SN9lNSVT1zf5aqmukLUoomjDU= +github.com/lightningnetwork/lnd v0.18.2-beta h1:Qv4xQ2ka05vqzmdkFdISHCHP6CzHoYNVKfD18XPjHsM= +github.com/lightningnetwork/lnd v0.18.2-beta/go.mod h1:cGQR1cVEZFZQcCx2VBbDY8xwGjCz+SupSopU1HpjP2I= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= github.com/lightningnetwork/lnd/clock v1.1.1/go.mod h1:mGnAhPyjYZQJmebS7aevElXKTFDuO+uNFFfMXK1W8xQ= github.com/lightningnetwork/lnd/fn v1.0.5 h1:ffDgMSn83avw6rNzxhbt6w5/2oIrwQKTPGfyaLupZtE= From dba4666e7b78a3f44477302ed09d82bd2fc1c18a Mon Sep 17 00:00:00 2001 From: Impa10r Date: Wed, 7 Aug 2024 23:05:07 +0200 Subject: [PATCH 10/98] Manual page refresh --- cmd/psweb/ln/claimjoin.go | 21 ++++++---- cmd/psweb/main.go | 14 +++---- cmd/psweb/telegram.go | 3 +- cmd/psweb/templates/bitcoin.gohtml | 65 +++++++++++++++--------------- 4 files changed, 54 insertions(+), 49 deletions(-) diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index dc923ce..62d2e14 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -331,7 +331,9 @@ func Broadcast(fromNodeId string, message *Message) error { if len(claimParties) < 2 && mathRand.Intn(2) > 0 { MyRole = "none" db.Save("ClaimJoin", "MyRole", MyRole) + log.Println("Initiator collision, switching to none") } else { + log.Println("Initiator collision, staying as initiator") break } } @@ -341,7 +343,7 @@ func Broadcast(fromNodeId string, message *Message) error { keyToNodeId[message.Sender] = fromNodeId // Time limit to apply is communicated via Amount ClaimBlockHeight = uint32(message.Amount) - ClaimStatus = "🧬 Received invitation to ClaimJoin" + ClaimStatus = "Received invitation to ClaimJoin" log.Println(ClaimStatus) case "pegin_ended": // only trust the message from the original handler @@ -350,7 +352,7 @@ func Broadcast(fromNodeId string, message *Message) error { if txId != "" { ClaimStatus = "ClaimJoin pegin successful! Liquid TxId: " + txId } else { - ClaimStatus = "Invitation to ClaimJoin revoked" + ClaimStatus = "Invitations to ClaimJoin revoked" } log.Println(ClaimStatus) resetClaimJoin() @@ -427,7 +429,7 @@ func Process(message *Message, senderNodeId string) { db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) } - log.Println("My pubKey:", myPublicKey()) + // log.Println("My pubKey:", myPublicKey()) if message.Destination == myPublicKey() { // Convert to []byte @@ -470,6 +472,7 @@ func Process(message *Message, senderNodeId string) { Status: status, }) { ClaimStatus = "Added new joiner, total participants: " + strconv.Itoa(len(claimParties)) + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) log.Println(ClaimStatus) } } else { @@ -571,7 +574,7 @@ func Process(message *Message, senderNodeId string) { ClaimStatus = "Kicked from ClaimJoin" log.Println(ClaimStatus) - db.Save("ClaimJoin", "PeginHandler", &PeginHandler) + db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) db.Save("ClaimJoin", "PeginHandler", &PeginHandler) // disable ClaimJoin @@ -618,6 +621,7 @@ func Process(message *Message, senderNodeId string) { EndClaimJoin("", "Coordination failure") } } + return } // message not to me, relay further @@ -822,7 +826,7 @@ func addClaimParty(newParty *ClaimParty) (bool, string) { } // verify claimBlockHeight - proof, err := bitcoin.GetTxOutProof(config.Config.PeginTxId) + proof, err := bitcoin.GetTxOutProof(newParty.TxId) if err != nil { return false, "Refused to join, TX not confirmed" } @@ -833,10 +837,11 @@ func addClaimParty(newParty *ClaimParty) (bool, string) { } txHeight := uint32(internet.GetTxHeight(newParty.TxId)) + claimHight := txHeight + PeginBlocks - if txHeight > 0 && txHeight+PeginBlocks != newParty.ClaimBlockHeight { - log.Printf("New joiner's ClaimBlockHeight was wrong") - newParty.ClaimBlockHeight = txHeight + PeginBlocks + if txHeight > 0 && claimHight != newParty.ClaimBlockHeight { + log.Printf("New joiner's ClaimBlockHeight was wrong: %d vs %d correct", newParty.ClaimBlockHeight, claimHight) + newParty.ClaimBlockHeight = claimHight } claimParties = append(claimParties, newParty) diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 6bd99ec..c5c6eff 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -1110,9 +1110,9 @@ func checkPegin() { if ln.MyRole != "none" { if currentBlockHeight > ln.ClaimBlockHeight+10 { // something is wrong, claim pegin individually - t := "🧬 ClaimJoin matured 10 blocks ago, switching to individual claim" + t := "ClaimJoin matured 10 blocks ago, switching to individual claim" log.Println(t) - telegramSendMessage(t) + telegramSendMessage("🧬 " + t) ln.MyRole = "none" config.Config.PeginClaimJoin = false config.Save() @@ -1124,15 +1124,15 @@ func checkPegin() { t := "" switch config.Config.PeginClaimScript { case "done": - t = "🧬 ClaimJoin pegin successfull! Liquid TxId: `" + config.Config.PeginTxId + "`" + t = "ClaimJoin pegin successfull! Liquid TxId: `" + config.Config.PeginTxId + "`" case "failed": - t = "🧬 ClaimJoin pegin failed: " + config.Config.PeginTxId + t = "ClaimJoin pegin failed: " + config.Config.PeginTxId default: goto singlePegin } // finish by sending telegram message - telegramSendMessage(t) + telegramSendMessage("🧬 " + t) config.Config.PeginClaimScript = "" config.Config.PeginTxId = "" config.Config.PeginClaimJoin = false @@ -1196,7 +1196,7 @@ singlePegin: if ln.PeginHandler == "" { // I will coordinate this handler if ln.InitiateClaimJoin(claimHeight) { - t := "🧬 Sent ClaimJoin invitations" + t := "Sent ClaimJoin invitations" log.Println(t) telegramSendMessage(t) @@ -1211,7 +1211,7 @@ singlePegin: } else if currentBlockHeight < ln.ClaimBlockHeight { // join by replying to initiator if ln.JoinClaimJoin(claimHeight) { - t := "🧬 Applied to ClaimJoin group" + t := "Applied to ClaimJoin group" log.Println(t) telegramSendMessage(t) } else { diff --git a/cmd/psweb/telegram.go b/cmd/psweb/telegram.go index 605322e..32afdef 100644 --- a/cmd/psweb/telegram.go +++ b/cmd/psweb/telegram.go @@ -105,7 +105,7 @@ func telegramStart() { if duration < 0 { formattedDuration = "Past due!" } - t = ln.ClaimStatus + t = "🧬 " + ln.ClaimStatus if ln.MyRole == "none" && ln.PeginHandler != "" { t += ". Time left to apply: " + formattedDuration } else if confs > 0 { @@ -243,7 +243,6 @@ func EscapeMarkdownV2(text string) string { "(", "\\(", ")", "\\)", "~", "\\~", - "`", "\\`", ">", "\\>", "#", "\\#", "+", "\\+", diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index 714158a..83ebcf6 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -5,33 +5,33 @@
-
-

 {{fmt .BitcoinBalance}} - {{if .BitcoinSwaps}} - ✔️ +
+

 {{fmt .BitcoinBalance}} + {{if .BitcoinSwaps}} + ✔️ + {{else}} + + {{end}} +

+
+
+
+ +

-
-
- - - - -
+ +
+
{{if eq .PeginTxId ""}}
@@ -116,7 +116,14 @@ {{else}}
{{if .IsPegin}} -

Peg-In Progress

+
+
+

Peg-In Progress

+
+
+

+
+
{{else}}

Bitcoin Withdrawal {{if gt .Confirmations 0}}(Complete){{else}}(Pending){{end}}

{{end}} @@ -159,7 +166,7 @@ {{if .IsClaimJoin}} - CJ: + 🧬 CJ: {{.ClaimJoinStatus}} @@ -226,12 +233,6 @@
- {{else}}
From ed6c18e2fd41a1afd60897d00ecf8b745ddc4ecb Mon Sep 17 00:00:00 2001 From: Impa10r Date: Thu, 8 Aug 2024 13:13:37 +0200 Subject: [PATCH 11/98] Works wih 2 --- CHANGELOG.md | 1 + cmd/psweb/handlers.go | 2 +- cmd/psweb/liquid/elements.go | 47 +++++ cmd/psweb/ln/claimjoin.go | 267 +++++++++++++++++------------ cmd/psweb/ln/cln.go | 1 + cmd/psweb/ln/common.go | 2 + cmd/psweb/ln/lnd.go | 46 +++-- cmd/psweb/main.go | 63 ++++--- cmd/psweb/templates/bitcoin.gohtml | 4 +- 9 files changed, 270 insertions(+), 163 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a851a88..3b1c793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.7.0 - Better estimate swap-in fee and maximum swap amount +- {TODO] Account for circular rebalancing cost and PPM ## 1.6.8 diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index ef332f1..e0409e6 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -880,7 +880,7 @@ func peginHandler(w http.ResponseWriter, r *http.Request) { } if isPegin { - log.Println("Peg-in TxId:", res.TxId, "RawHex:", res.RawHex, "Claim script:", claimScript) + log.Println("New Peg-in TxId:", res.TxId, "RawHex:", res.RawHex, "Claim script:", claimScript) duration := time.Duration(10*ln.PeginBlocks) * time.Minute formattedDuration := time.Time{}.Add(duration).Format("15h 04m") telegramSendMessage("⏰ Started peg-in " + formatWithThousandSeparators(uint64(res.AmountSat)) + " sats. Time left: " + formattedDuration + ". TxId: `" + res.TxId + "`") diff --git a/cmd/psweb/liquid/elements.go b/cmd/psweb/liquid/elements.go index 74cc0cd..cb495f1 100644 --- a/cmd/psweb/liquid/elements.go +++ b/cmd/psweb/liquid/elements.go @@ -725,3 +725,50 @@ func SendRawTransaction(hexTx string) (string, error) { return response, nil } + +type AddressInfo struct { + Address string `json:"address"` + ScriptPubKey string `json:"scriptPubKey"` + IsMine bool `json:"ismine"` + Solvable bool `json:"solvable"` + Desc string `json:"desc"` + IsWatchOnly bool `json:"iswatchonly"` + IsScript bool `json:"isscript"` + IsWitness bool `json:"iswitness"` + WitnessVersion int `json:"witness_version"` + WitnessProgram string `json:"witness_program"` + Pubkey string `json:"pubkey"` + Confidential string `json:"confidential"` + ConfidentialKey string `json:"confidential_key"` + Unconfidential string `json:"unconfidential"` + IsChange bool `json:"ischange"` + Timestamp int64 `json:"timestamp"` + HDKeyPath string `json:"hdkeypath"` + HDSeedID string `json:"hdseedid"` + HDMasterFingerprint string `json:"hdmasterfingerprint"` + Labels []string `json:"labels"` +} + +func GetAddressInfo(addr, wallet string) (*AddressInfo, error) { + + client := ElementsClient() + service := &Elements{client} + + params := []interface{}{addr} + + r, err := service.client.call("getaddressinfo", params, "/wallet/"+wallet) + if err = handleError(err, &r); err != nil { + log.Printf("Failed to get address info: %v", err) + return nil, err + } + + var response AddressInfo + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("GetAddressInfo unmarshall: %v", err) + return nil, err + } + + return &response, nil +} diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 62d2e14..f3148b2 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -3,6 +3,8 @@ package ln import ( + "crypto/aes" + "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" @@ -11,7 +13,6 @@ import ( "io" "log" "strconv" - "time" mathRand "math/rand" @@ -29,8 +30,8 @@ import ( // maximum number of participants in ClaimJoin const ( - maxParties = 2 - PeginBlocks = 10 //2 + maxParties = 5 + PeginBlocks = 10 //102 ) var ( @@ -47,7 +48,7 @@ var ( // none, initiator or joiner MyRole = "none" // array of initiator + joiners, for initiator only - claimParties []*ClaimParty + ClaimParties []*ClaimParty // PSET to be blinded and signed by all parties claimPSET string ) @@ -77,13 +78,12 @@ type ClaimParty struct { // when can be claimed ClaimBlockHeight uint32 // to be filled locally by initiator - RawTx string - TxoutProof string - Amount uint64 - FeeShare uint64 - PubKey string - LastMessage *Coordination - SentTime time.Time + RawTx string + TxoutProof string + Amount uint64 + FeeShare uint64 + PubKey string + SentCount uint } // runs after restart, to continue if pegin is ongoing @@ -94,7 +94,7 @@ func loadClaimJoinDB() { db.Load("ClaimJoin", "MyRole", &MyRole) db.Load("ClaimJoin", "keyToNodeId", &keyToNodeId) - db.Load("ClaimJoin", "claimParties", &claimParties) + db.Load("ClaimJoin", "claimParties", &ClaimParties) if MyRole == "initiator" { db.Load("ClaimJoin", "claimPSET", &claimPSET) @@ -107,7 +107,7 @@ func loadClaimJoinDB() { // runs every minute func OnTimer() { - if !config.Config.PeginClaimJoin || MyRole != "initiator" || len(claimParties) < 2 { + if !config.Config.PeginClaimJoin || MyRole != "initiator" || len(ClaimParties) < 2 { return } @@ -122,7 +122,7 @@ func OnTimer() { return } - startingFee := 36 * len(claimParties) + startingFee := 36*len(ClaimParties) - 1 if claimPSET == "" { var err error @@ -146,7 +146,7 @@ func OnTimer() { for i, output := range analyzed.Outputs { if output.Blind && output.Status == "unblinded" { blinder := decoded.Outputs[i].BlinderIndex - ClaimStatus = "Blinding " + strconv.Itoa(i+1) + "/" + strconv.Itoa(len(claimParties)) + ClaimStatus = "Blinding " + strconv.Itoa(i+1) + "/" + strconv.Itoa(len(ClaimParties)) log.Println(ClaimStatus) @@ -160,32 +160,34 @@ func OnTimer() { } } else { action := "process" - if i == len(claimParties)-1 { + if i == len(ClaimParties)-1 { // the final blinder can blind and sign at once action = "process2" - ClaimStatus = "Signing " + strconv.Itoa(len(claimParties)) + "/" + strconv.Itoa(len(claimParties)) - log.Println(ClaimStatus) + ClaimStatus = "Signing " + strconv.Itoa(len(ClaimParties)) + "/" + strconv.Itoa(len(ClaimParties)) } - if !SendCoordination(claimParties[blinder].PubKey, &Coordination{ - Action: action, - PSET: claimPSET, - Status: ClaimStatus, - ClaimBlockHeight: ClaimBlockHeight, - }) { - log.Println("Unable to send blind coordination, cancelling ClaimJoin") - EndClaimJoin("", "Coordination failure") + if checkPeerStatus(blinder) { + if !SendCoordination(ClaimParties[blinder].PubKey, &Coordination{ + Action: action, + PSET: claimPSET, + Status: ClaimStatus, + ClaimBlockHeight: ClaimBlockHeight, + }) { + log.Println("Unable to send coordination, cancelling ClaimJoin") + EndClaimJoin("", "Coordination failure") + } } + return } } } // Iterate through inputs in reverse order to sign - for i := len(claimParties) - 1; i >= 0; i-- { + for i := len(ClaimParties) - 1; i >= 0; i-- { input := decoded.Inputs[i] if len(input.FinalScriptWitness) == 0 { - ClaimStatus = "Signing " + strconv.Itoa(len(claimParties)-i) + "/" + strconv.Itoa(len(claimParties)) + ClaimStatus = "Signing " + strconv.Itoa(len(ClaimParties)-i) + "/" + strconv.Itoa(len(ClaimParties)) log.Println(ClaimStatus) @@ -198,15 +200,18 @@ func OnTimer() { return } } else { - if !SendCoordination(claimParties[i].PubKey, &Coordination{ - Action: "process", - PSET: claimPSET, - Status: ClaimStatus, - ClaimBlockHeight: ClaimBlockHeight, - }) { - log.Println("Unable to send blind coordination, cancelling ClaimJoin") - EndClaimJoin("", "Coordination failure") + if checkPeerStatus(i) { + if !SendCoordination(ClaimParties[i].PubKey, &Coordination{ + Action: "process", + PSET: claimPSET, + Status: ClaimStatus, + ClaimBlockHeight: ClaimBlockHeight, + }) { + log.Println("Unable to send blind coordination, cancelling ClaimJoin") + EndClaimJoin("", "Coordination failure") + } } + return } } @@ -278,22 +283,59 @@ func OnTimer() { } } +func checkPeerStatus(i int) bool { + ClaimParties[i].SentCount++ + if ClaimParties[i].SentCount > 10 { + // peer is offline, kick him + kickPeer(ClaimParties[i].PubKey, "being unresponsive") + return false + } + return true +} + +func kickPeer(pubKey, reason string) { + if ok := removeClaimParty(pubKey); ok { + ClaimStatus = "Joiner " + pubKey + " kicked, total participants: " + strconv.Itoa(len(ClaimParties)) + // persist to db + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + log.Println(ClaimStatus) + // erase PSET to start over + claimPSET = "" + // persist to db + db.Save("ClaimJoin", "claimPSET", claimPSET) + // inform the offender + SendCoordination(pubKey, &Coordination{ + Action: "refuse_add", + Status: "Kicked for " + reason, + }) + } else { + EndClaimJoin("", "Coordination failure") + } +} + // Called when received a broadcast custom message // Forward the message to all direct peers, unless the source key is known already // (it means the message came back to you from a downstream peer) func Broadcast(fromNodeId string, message *Message) error { - if message.Asset == "pegin_started" && keyToNodeId[message.Sender] != "" || message.Asset == "pegin_ended" && PeginHandler == "" { - // has been previously received from upstream - return nil - } - client, cleanup, err := ps.GetClient(config.Config.RpcHost) if err != nil { return err } defer cleanup() + if myNodeId == "" { + // populates myNodeId + if getLndVersion() == 0 { + return fmt.Errorf("Broadcast: Cannot get myNodeId") + } + } + + if fromNodeId != myNodeId && (message.Asset == "pegin_started" && keyToNodeId[message.Sender] != "" || message.Asset == "pegin_ended" && PeginHandler == "") { + // has been previously received from upstream, ignore + return nil + } + res, err := ps.ListPeers(client) if err != nil { return err @@ -312,23 +354,22 @@ func Broadcast(fromNodeId string, message *Message) error { } } - if myNodeId == "" { - // populates myNodeId - if getLndVersion() == 0 { - return fmt.Errorf("Broadcast: Cannot get myNodeId") - } - } - if fromNodeId == myNodeId { // do nothing more after sending the original broadcast return nil } + if keyToNodeId[message.Sender] != fromNodeId { + // store for relaying further encrypted messages + keyToNodeId[message.Sender] = fromNodeId + db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) + } + switch message.Asset { case "pegin_started": if MyRole == "initiator" { - // two simultaneous initiators conflict, agree if I have no joiners with 50% chance - if len(claimParties) < 2 && mathRand.Intn(2) > 0 { + // two simultaneous initiators conflict + if len(ClaimParties) < 2 { MyRole = "none" db.Save("ClaimJoin", "MyRole", MyRole) log.Println("Initiator collision, switching to none") @@ -336,37 +377,40 @@ func Broadcast(fromNodeId string, message *Message) error { log.Println("Initiator collision, staying as initiator") break } + } else if MyRole == "joiner" { + // already joined another group, ignore + return nil } + // where to forward claimjoin request PeginHandler = message.Sender - // store for relaying further encrypted messages - keyToNodeId[message.Sender] = fromNodeId // Time limit to apply is communicated via Amount ClaimBlockHeight = uint32(message.Amount) ClaimStatus = "Received invitation to ClaimJoin" log.Println(ClaimStatus) + + // persist to db + db.Save("ClaimJoin", "PeginHandler", PeginHandler) + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + case "pegin_ended": // only trust the message from the original handler - if PeginHandler == message.Sender { + if MyRole == "joiner" && PeginHandler == message.Sender { txId := message.Payload if txId != "" { ClaimStatus = "ClaimJoin pegin successful! Liquid TxId: " + txId + log.Println(ClaimStatus) + // signal to telegram bot + config.Config.PeginTxId = txId + config.Config.PeginClaimScript = "done" } else { ClaimStatus = "Invitations to ClaimJoin revoked" } log.Println(ClaimStatus) resetClaimJoin() - return nil } } - - // persist to db - db.Save("ClaimJoin", "MyRole", MyRole) - db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) - db.Save("ClaimJoin", "PeginHandler", PeginHandler) - db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) - db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - return nil } @@ -471,7 +515,7 @@ func Process(message *Message, senderNodeId string) { ClaimBlockHeight: ClaimBlockHeight, Status: status, }) { - ClaimStatus = "Added new joiner, total participants: " + strconv.Itoa(len(claimParties)) + ClaimStatus = "Added new joiner, total participants: " + strconv.Itoa(len(ClaimParties)) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) log.Println(ClaimStatus) } @@ -491,7 +535,7 @@ func Process(message *Message, senderNodeId string) { } if removeClaimParty(msg.Joiner.PubKey) { - ClaimStatus = "Removed a joiner, total participants: " + strconv.Itoa(len(claimParties)) + ClaimStatus = "Removed a joiner, total participants: " + strconv.Itoa(len(ClaimParties)) // persist to db db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) log.Println(ClaimStatus) @@ -544,36 +588,22 @@ func Process(message *Message, senderNodeId string) { log.Println("PSET verification failure!") if MyRole == "initiator" { // kick the joiner who returned broken PSET - if ok := removeClaimParty(message.Sender); ok { - ClaimStatus = "Joiner " + message.Sender + " kicked, total participants: " + strconv.Itoa(len(claimParties)) - // persist to db - db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) - log.Println(ClaimStatus) - // erase PSET to start over - claimPSET = "" - // persist to db - db.Save("ClaimJoin", "claimPSET", claimPSET) - - SendCoordination(message.Sender, &Coordination{ - Action: "refuse_add", - Status: "Kicked for broken PSET return", - }) - } else { - EndClaimJoin("", "Coordination failure") - } + kickPeer(message.Sender, "broken PSET return") return } else { // remove yourself from ClaimJoin if SendCoordination(PeginHandler, &Coordination{ Action: "remove", - Joiner: *claimParties[0], + Joiner: *ClaimParties[0], }) { ClaimBlockHeight = 0 // forget pegin handler, so that cannot initiate new ClaimJoin PeginHandler = "" - ClaimStatus = "Kicked from ClaimJoin" + ClaimStatus = "Left ClaimJoin group" + MyRole = "none" log.Println(ClaimStatus) + db.Save("ClaimJoin", "MyRole", &MyRole) db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) db.Save("ClaimJoin", "PeginHandler", &PeginHandler) @@ -586,6 +616,14 @@ func Process(message *Message, senderNodeId string) { } if MyRole == "initiator" { + // reset SentCount + for i, party := range ClaimParties { + if party.PubKey == message.Sender { + ClaimParties[i].SentCount = 0 + break + } + } + // Save received claimPSET, execute OnTimer db.Save("ClaimJoin", "claimPSET", &claimPSET) OnTimer() @@ -665,13 +703,13 @@ func InitiateClaimJoin(claimBlockHeight uint32) bool { party := createClaimParty(claimBlockHeight) if party != nil { // initiate array of claim parties - claimParties = nil - claimParties = append(claimParties, party) + ClaimParties = nil + ClaimParties = append(ClaimParties, party) ClaimStatus = "Invites sent, awaiting joiners" // persist to db db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - db.Save("ClaimJoin", "claimParties", claimParties) + db.Save("ClaimJoin", "claimParties", ClaimParties) return true } } @@ -702,9 +740,9 @@ func EndClaimJoin(txId string, status string) bool { config.Config.PeginTxId = txId config.Config.PeginClaimScript = "done" } else { - log.Println("ClaimJoin failed") - // signal to telegram bot - config.Config.PeginClaimScript = "failed" + log.Println("ClaimJoin failed. Switching back to individual.") + config.Config.PeginClaimJoin = false + config.Save() } resetClaimJoin() return true @@ -715,14 +753,14 @@ func EndClaimJoin(txId string, status string) bool { func resetClaimJoin() { // eraze all traces ClaimBlockHeight = 0 - claimParties = nil + ClaimParties = nil MyRole = "none" PeginHandler = "" ClaimStatus = "No ClaimJoin peg-in is pending" keyToNodeId = make(map[string]string) // persist to db - db.Save("ClaimJoin", "claimParties", claimParties) + db.Save("ClaimJoin", "claimParties", ClaimParties) db.Save("ClaimJoin", "PeginHandler", PeginHandler) db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) @@ -755,12 +793,12 @@ func JoinClaimJoin(claimBlockHeight uint32) bool { ClaimBlockHeight: claimBlockHeight, }) { // initiate array of claim parties for single entry - claimParties = nil - claimParties = append(claimParties, party) + ClaimParties = nil + ClaimParties = append(ClaimParties, party) ClaimStatus = "Responded to invitation, awaiting confirmation" // persist to db db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - db.Save("ClaimJoin", "claimParties", claimParties) + db.Save("ClaimJoin", "claimParties", ClaimParties) ClaimBlockHeight = claimBlockHeight return true } @@ -814,14 +852,14 @@ func createClaimParty(claimBlockHeight uint32) *ClaimParty { // add claim party to the list func addClaimParty(newParty *ClaimParty) (bool, string) { - for _, party := range claimParties { + for _, party := range ClaimParties { if party.ClaimScript == newParty.ClaimScript { // is already in the list - return true, "Was already in the list, total participants: " + strconv.Itoa(len(claimParties)) + return true, "Was already in the list, total participants: " + strconv.Itoa(len(ClaimParties)) } } - if len(claimParties) == maxParties { + if len(ClaimParties) == maxParties { return false, "Refused to join, over limit of " + strconv.Itoa(maxParties) } @@ -837,19 +875,19 @@ func addClaimParty(newParty *ClaimParty) (bool, string) { } txHeight := uint32(internet.GetTxHeight(newParty.TxId)) - claimHight := txHeight + PeginBlocks + claimHight := txHeight + PeginBlocks - 1 if txHeight > 0 && claimHight != newParty.ClaimBlockHeight { log.Printf("New joiner's ClaimBlockHeight was wrong: %d vs %d correct", newParty.ClaimBlockHeight, claimHight) newParty.ClaimBlockHeight = claimHight } - claimParties = append(claimParties, newParty) + ClaimParties = append(ClaimParties, newParty) // persist to db - db.Save("ClaimJoin", "claimParties", claimParties) + db.Save("ClaimJoin", "claimParties", ClaimParties) - return true, "Successfully joined, total participants: " + strconv.Itoa(len(claimParties)) + return true, "Successfully joined as participant #" + strconv.Itoa(len(ClaimParties)) } // remove claim party from the list by public key @@ -858,7 +896,7 @@ func removeClaimParty(pubKey string) bool { found := false claimBlockHeight := uint32(0) - for _, party := range claimParties { + for _, party := range ClaimParties { if party.PubKey == pubKey { found = true } else { @@ -877,10 +915,10 @@ func removeClaimParty(pubKey string) bool { db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) } - claimParties = newClaimParties + ClaimParties = newClaimParties // persist to db - db.Save("ClaimJoin", "claimParties", claimParties) + db.Save("ClaimJoin", "claimParties", ClaimParties) return true } @@ -900,8 +938,7 @@ func myPublicKey() string { return publicKeyToBase64(myPrivateKey.PubKey()) } -/* -// Payload is encrypted with a new symmetric AES key and returned as base64 string +// Payload is encrypted with a symmetric AES key and returned as base64 string // The AES key is encrypted using destination secp256k1 pubKey, returned as base64 func encryptPayload(pubKeyString string, plaintext []byte) (string, string, error) { // Generate AES key @@ -939,8 +976,6 @@ func encryptPayload(pubKeyString string, plaintext []byte) (string, string, erro return base64cypherText, encryptedKey, nil } -*/ - // Encrypt with base64 public key func eciesEncrypt(pubKeyString string, message []byte) (string, error) { @@ -1031,10 +1066,10 @@ func createClaimPSET(totalFee int) (string, error) { var outputs []map[string]interface{} feeToSplit := totalFee - feePart := totalFee / len(claimParties) + feePart := totalFee / len(ClaimParties) // fill in the arrays - for i, party := range claimParties { + for i, party := range ClaimParties { input := map[string]interface{}{ "txid": party.TxId, "vout": party.Vout, @@ -1043,7 +1078,7 @@ func createClaimPSET(totalFee int) (string, error) { "pegin_claim_script": party.ClaimScript, } - if i == len(claimParties)-1 { + if i == len(ClaimParties)-1 { // the last joiner pays higher fee if it cannot be divided equally feePart = feeToSplit } @@ -1096,11 +1131,19 @@ func verifyPSET() bool { return false } + addressInfo, err := liquid.GetAddressInfo(ClaimParties[0].Address, config.Config.ElementsWallet) + if err != nil { + return false + } + for _, output := range decoded.Outputs { // 50 sats maximum fee allowed - if output.Script.Address == claimParties[0].Address && liquid.ToBitcoin(claimParties[0].Amount)-output.Amount < 0.0000005 { + if output.Script.Address == addressInfo.Unconfidential && liquid.ToBitcoin(ClaimParties[0].Amount)-output.Amount < 0.0000005 { return true } } + + log.Println(claimPSET) + return false } diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index 5d9d2f6..683d4b9 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -1322,6 +1322,7 @@ var ( MyRole = "none" ClaimStatus = "" ClaimBlockHeight = uint32(0) + ClaimParties = []int ) const PeginBlocks = 102 diff --git a/cmd/psweb/ln/common.go b/cmd/psweb/ln/common.go index b3f578d..3e4c261 100644 --- a/cmd/psweb/ln/common.go +++ b/cmd/psweb/ln/common.go @@ -54,6 +54,8 @@ type ChannelStats struct { PaidOut uint64 InvoicedIn uint64 PaidCost uint64 + RebalanceIn uint64 + RebalanceCost uint64 } type ChanneInfo struct { diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 760deef..347e84a 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -57,10 +57,11 @@ var ( LndVerson = float64(0) // must be 0.18+ for RBF ability // arrays mapped per channel - forwardsIn = make(map[uint64][]*lnrpc.ForwardingEvent) - forwardsOut = make(map[uint64][]*lnrpc.ForwardingEvent) - paymentHtlcs = make(map[uint64][]*lnrpc.HTLCAttempt) - invoiceHtlcs = make(map[uint64][]*lnrpc.InvoiceHTLC) + forwardsIn = make(map[uint64][]*lnrpc.ForwardingEvent) + forwardsOut = make(map[uint64][]*lnrpc.ForwardingEvent) + paymentHtlcs = make(map[uint64][]*lnrpc.HTLCAttempt) + rebalanceHtlcs = make(map[uint64][]*lnrpc.HTLCAttempt) + invoiceHtlcs = make(map[uint64][]*lnrpc.InvoiceHTLC) // inflight HTLCs mapped per Incoming channel inflightHTLCs = make(map[uint64][]*InflightHTLC) @@ -895,6 +896,9 @@ func appendPayment(payment *lnrpc.Payment) { // get channel from the first hop chanId := htlc.Route.Hops[0].ChanId paymentHtlcs[chanId] = append(paymentHtlcs[chanId], htlc) + // get destination from the last hop + chanId = htlc.Route.Hops[len(htlc.Route.Hops)-1].ChanId + rebalanceHtlcs[chanId] = append(rebalanceHtlcs[chanId], htlc) } } // store the last timestamp @@ -1215,8 +1219,8 @@ func subscribeMessages(ctx context.Context, client lnrpc.LightningClient) error // received request for information if msg.Memo == "poll" { - if MyRole == "initiator" && myPublicKey() != "" && len(claimParties) < maxParties { - // repeat pegin start broadcast + if MyRole == "initiator" && myPublicKey() != "" && len(ClaimParties) < maxParties && GetBlockHeight(client) < ClaimBlockHeight { + // repeat pegin start info SendCustomMessage(client, nodeId, &Message{ Version: MessageVersion, Memo: "broadcast", @@ -1432,14 +1436,16 @@ func GetChannelInfo(client lnrpc.LightningClient, channelId uint64, peerNodeId s func GetChannelStats(channelId uint64, timeStamp uint64) *ChannelStats { var ( - result ChannelStats - routedInMsat uint64 - routedOutMsat uint64 - feeMsat uint64 - assistedMsat uint64 - paidOutMsat int64 - invoicedMsat uint64 - costMsat int64 + result ChannelStats + routedInMsat uint64 + routedOutMsat uint64 + feeMsat uint64 + assistedMsat uint64 + paidOutMsat int64 + invoicedMsat uint64 + costMsat int64 + rebalanceMsat int64 + rebalanceCostMsat int64 ) timestampNs := timeStamp * 1_000_000_000 @@ -1450,17 +1456,20 @@ func GetChannelStats(channelId uint64, timeStamp uint64) *ChannelStats { feeMsat += e.FeeMsat } } + for _, e := range forwardsIn[channelId] { if e.TimestampNs > timestampNs && e.AmtOutMsat > ignoreForwardsMsat { routedInMsat += e.AmtInMsat assistedMsat += e.FeeMsat } } + for _, e := range invoiceHtlcs[channelId] { if uint64(e.AcceptTime) > timeStamp { invoicedMsat += e.AmtMsat } } + for _, e := range paymentHtlcs[channelId] { if uint64(e.AttemptTimeNs) > timestampNs { paidOutMsat += e.Route.TotalAmtMsat @@ -1468,6 +1477,13 @@ func GetChannelStats(channelId uint64, timeStamp uint64) *ChannelStats { } } + for _, e := range rebalanceHtlcs[channelId] { + if uint64(e.AttemptTimeNs) > timestampNs { + rebalanceMsat += e.Route.TotalAmtMsat + rebalanceCostMsat += e.Route.TotalFeesMsat + } + } + result.RoutedOut = routedOutMsat / 1000 result.RoutedIn = routedInMsat / 1000 result.FeeSat = feeMsat / 1000 @@ -1475,6 +1491,8 @@ func GetChannelStats(channelId uint64, timeStamp uint64) *ChannelStats { result.InvoicedIn = invoicedMsat / 1000 result.PaidOut = uint64(paidOutMsat / 1000) result.PaidCost = uint64(costMsat / 1000) + result.RebalanceIn = uint64(rebalanceMsat / 1000) + result.RebalanceCost = uint64(rebalanceCostMsat / 1000) return &result } diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index c5c6eff..fc45dfa 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -380,10 +380,12 @@ func onTimer(firstRun bool) { if !firstRun { // skip first run so that forwards have time to download go ln.ApplyAutoFees() - // Check if pegin can be claimed or joined - go checkPegin() + } + // Check if pegin can be claimed or joined + go checkPegin() + // advertise Liquid balance go advertiseBalances() @@ -1088,10 +1090,6 @@ func convertSwapsToHTMLTable(swaps []*peerswaprpc.PrettyPrintSwap, nodeId string // Check Peg-in status func checkPegin() { - if config.Config.PeginTxId == "" { - return - } - cl, clean, er := ln.GetClient() if er != nil { return @@ -1100,17 +1098,26 @@ func checkPegin() { currentBlockHeight := ln.GetBlockHeight(cl) - if ln.MyRole == "none" && currentBlockHeight > ln.ClaimBlockHeight { + if currentBlockHeight > ln.ClaimBlockHeight && ln.MyRole == "none" && ln.PeginHandler != "" { // invitation expired ln.PeginHandler = "" db.Save("ClaimJoin", "PeginHandler", ln.PeginHandler) } + if config.Config.PeginTxId == "" { + return + } + if config.Config.PeginClaimJoin { if ln.MyRole != "none" { - if currentBlockHeight > ln.ClaimBlockHeight+10 { - // something is wrong, claim pegin individually - t := "ClaimJoin matured 10 blocks ago, switching to individual claim" + // blocks to wait before switching back to indivicual claim + margin := uint32(2) + if ln.MyRole == "initiator" && len(ln.ClaimParties) < 2 { + margin = 1 + } + if currentBlockHeight > ln.ClaimBlockHeight+margin { + // claim pegin individually + t := "ClaimJoin matured, switching to individual claim" log.Println(t) telegramSendMessage("🧬 " + t) ln.MyRole = "none" @@ -1121,27 +1128,17 @@ func checkPegin() { return } - t := "" - switch config.Config.PeginClaimScript { - case "done": - t = "ClaimJoin pegin successfull! Liquid TxId: `" + config.Config.PeginTxId + "`" - case "failed": - t = "ClaimJoin pegin failed: " + config.Config.PeginTxId - default: - goto singlePegin + if config.Config.PeginClaimScript == "done" { + // finish by sending telegram message + telegramSendMessage("🧬 ClaimJoin pegin successfull! Liquid TxId: `" + config.Config.PeginTxId + "`") + config.Config.PeginClaimScript = "" + config.Config.PeginTxId = "" + config.Config.PeginClaimJoin = false + config.Save() + return } - - // finish by sending telegram message - telegramSendMessage("🧬 " + t) - config.Config.PeginClaimScript = "" - config.Config.PeginTxId = "" - config.Config.PeginClaimJoin = false - config.Save() - return } -singlePegin: - confs, _ := ln.GetTxConfirmations(cl, config.Config.PeginTxId) if confs < 0 && config.Config.PeginReplacedTxId != "" { confs, _ = ln.GetTxConfirmations(cl, config.Config.PeginReplacedTxId) @@ -1156,7 +1153,7 @@ singlePegin: if confs > 0 { if config.Config.PeginClaimScript == "" { log.Println("BTC withdrawal complete, txId: " + config.Config.PeginTxId) - telegramSendMessage("BTC withdrawal complete. TxId: `" + config.Config.PeginTxId + "`") + telegramSendMessage("💸 BTC withdrawal complete. TxId: `" + config.Config.PeginTxId + "`") } else if confs >= ln.PeginBlocks && ln.MyRole == "none" { // claim individual pegin failed := false @@ -1194,14 +1191,12 @@ singlePegin: if ln.MyRole == "none" { claimHeight := currentBlockHeight + ln.PeginBlocks - uint32(confs) if ln.PeginHandler == "" { - // I will coordinate this handler + // I will coordinate this join if ln.InitiateClaimJoin(claimHeight) { t := "Sent ClaimJoin invitations" log.Println(t) - telegramSendMessage(t) - + telegramSendMessage("🧬 " + t) ln.MyRole = "initiator" - // persist to db db.Save("ClaimJoin", "MyRole", ln.MyRole) } else { log.Println("Failed to initiate ClaimJoin, continuing as a single pegin") @@ -1213,7 +1208,7 @@ singlePegin: if ln.JoinClaimJoin(claimHeight) { t := "Applied to ClaimJoin group" log.Println(t) - telegramSendMessage(t) + telegramSendMessage("🧬 " + t) } else { log.Println("Failed to apply to ClaimJoin, continuing as a single pegin") config.Config.PeginClaimJoin = false diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index 83ebcf6..8cf7c19 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -116,7 +116,7 @@ {{else}}
{{if .IsPegin}} -
+

Peg-In Progress

@@ -127,7 +127,7 @@ {{else}}

Bitcoin Withdrawal {{if gt .Confirmations 0}}(Complete){{else}}(Pending){{end}}

{{end}} - +
- + From f042d9685a75a864f10c913075d8f82be3d20147 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Sat, 24 Aug 2024 13:11:34 +0200 Subject: [PATCH 40/98] Circular cost lnd --- .vscode/launch.json | 4 ++-- CHANGELOG.md | 2 +- cmd/psweb/handlers.go | 4 ++-- cmd/psweb/ln/lnd.go | 6 +++--- cmd/psweb/main.go | 32 +++++++++++++++++++------------- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2c86c18..2451d17 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,8 +13,8 @@ "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - "args": ["-datadir", "/home/vlad/.peerswap_t4"] - //"args": ["-datadir", "/home/vlad/.peerswap2"] + //"args": ["-datadir", "/home/vlad/.peerswap_t4"] + "args": ["-datadir", "/home/vlad/.peerswap3"] } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index dbd13f4..e333e17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Switch Custom Message serialization from JSON to GOB - Switch Custom Message type from 42067 to 42065 - Fix accounting for initiated swap outs and failed swaps -- {TODO] Account for circular rebalancing cost and PPM +- Show circular rebalancing cost and PPM instead of paid fees ## 1.6.8 diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index 813ff36..c5ee918 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -760,8 +760,8 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { FeeRate: config.Config.PeginFeeRate, MempoolFeeRate: mempoolFeeRate, LiquidFeeRate: liquid.EstimateFee(), - SuggestedFeeRate: math.Ceil(fee*10) / 10, - MinBumpFeeRate: math.Ceil((config.Config.PeginFeeRate+1)*10) / 10, + SuggestedFeeRate: math.Ceil(fee*100) / 100, + MinBumpFeeRate: math.Ceil((config.Config.PeginFeeRate+1)*100) / 100, CanBump: canBump, CanRBF: ln.CanRBF(), IsCLN: ln.Implementation == "CLN", diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 3317434..0c58471 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -407,7 +407,7 @@ finalize: _, err = cl.PublishTransaction(ctx, req) if err != nil { log.Println("PublishTransaction:", err) - log.Println("RawHex:", hex.EncodeToString(rawTx)) + // log.Println("RawHex:", hex.EncodeToString(rawTx)) releaseOutputs(cl, utxos, &lockId) return nil, err } @@ -655,7 +655,7 @@ func BumpPeginFee(feeRate float64, label string) (*SentResult, error) { var errr error // extra bump may be necessary if the tx does not pay enough fee - for extraBump := float64(0); extraBump <= 1; extraBump += 0.1 { + for extraBump := float64(0); extraBump <= 1; extraBump += 0.01 { // sometimes remove transaction is not enough releaseOutputs(cl, &utxos, &internalLockId) releaseOutputs(cl, &utxos, &myLockId) @@ -1582,7 +1582,7 @@ func GetChannelStats(channelId uint64, timeStamp uint64) *ChannelStats { for _, e := range rebalanceHtlcs[channelId] { if uint64(e.AttemptTimeNs) > timestampNs { - rebalanceMsat += e.Route.TotalAmtMsat + rebalanceMsat += e.Route.TotalAmtMsat - e.Route.TotalFeesMsat rebalanceCostMsat += e.Route.TotalFeesMsat } } diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 6084899..6c04f9b 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -520,9 +520,11 @@ func convertPeersToHTMLTable( var totalForwardsIn uint64 var totalPayments uint64 var totalFees uint64 - var totalCost uint64 + var rebalanceCost uint64 + var rebalanceAmount uint64 channelsTable := "
Amount: From fc5cbbe263b30a09421579ccda5fc906e0d1e558 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Thu, 8 Aug 2024 14:50:17 +0200 Subject: [PATCH 12/98] fixed done --- cmd/psweb/main.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index fc45dfa..631b695 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -1109,6 +1109,16 @@ func checkPegin() { } if config.Config.PeginClaimJoin { + if config.Config.PeginClaimScript == "done" { + // finish by sending telegram message + telegramSendMessage("🧬 ClaimJoin pegin successfull! Liquid TxId: `" + config.Config.PeginTxId + "`") + config.Config.PeginClaimScript = "" + config.Config.PeginTxId = "" + config.Config.PeginClaimJoin = false + config.Save() + return + } + if ln.MyRole != "none" { // blocks to wait before switching back to indivicual claim margin := uint32(2) @@ -1123,20 +1133,10 @@ func checkPegin() { ln.MyRole = "none" config.Config.PeginClaimJoin = false config.Save() - ln.EndClaimJoin("", "Something went wrong") + ln.EndClaimJoin("", "Claimed individually") } return } - - if config.Config.PeginClaimScript == "done" { - // finish by sending telegram message - telegramSendMessage("🧬 ClaimJoin pegin successfull! Liquid TxId: `" + config.Config.PeginTxId + "`") - config.Config.PeginClaimScript = "" - config.Config.PeginTxId = "" - config.Config.PeginClaimJoin = false - config.Save() - return - } } confs, _ := ln.GetTxConfirmations(cl, config.Config.PeginTxId) From 3b21c8e060623bda487d5afda27d6f3afc7b6a32 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Thu, 8 Aug 2024 20:45:56 +0200 Subject: [PATCH 13/98] Changed to GOB --- .vscode/launch.json | 2 +- CHANGELOG.md | 2 + cmd/psweb/internet/bitcoin.go | 2 +- cmd/psweb/internet/liquid.go | 2 +- cmd/psweb/ln/claimjoin.go | 290 +++++++++++++++++----------------- cmd/psweb/ln/common.go | 20 +-- cmd/psweb/ln/lnd.go | 25 +-- cmd/psweb/main.go | 2 +- 8 files changed, 174 insertions(+), 171 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2c86c18..9d86866 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,7 @@ "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - "args": ["-datadir", "/home/vlad/.peerswap_t4"] + "args": ["-datadir", "/home/vlad/.peerswap2_t4"] //"args": ["-datadir", "/home/vlad/.peerswap2"] } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b1c793..e16c705 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 1.7.0 +- Switched Custom Message serialization from JSON to GOB +- Switched Custom Message type from 42067 to 42066 - Better estimate swap-in fee and maximum swap amount - {TODO] Account for circular rebalancing cost and PPM diff --git a/cmd/psweb/internet/bitcoin.go b/cmd/psweb/internet/bitcoin.go index 83482c2..39ae0c9 100644 --- a/cmd/psweb/internet/bitcoin.go +++ b/cmd/psweb/internet/bitcoin.go @@ -21,7 +21,7 @@ func GetNodeAlias(id string) string { return id[:20] // shortened id } resp, err2 := cl.Do(req) - if err2 == nil { + if err2 == nil && resp.StatusCode == http.StatusOK { defer resp.Body.Close() buf := new(bytes.Buffer) _, _ = buf.ReadFrom(resp.Body) diff --git a/cmd/psweb/internet/liquid.go b/cmd/psweb/internet/liquid.go index c32596f..284b752 100644 --- a/cmd/psweb/internet/liquid.go +++ b/cmd/psweb/internet/liquid.go @@ -21,7 +21,7 @@ func GetLiquidTxFee(txid string) int64 { return 0 } resp, err2 := cl.Do(req) - if err2 == nil { + if err2 == nil && resp.StatusCode == http.StatusOK { defer resp.Body.Close() var tx map[string]interface{} diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index f3148b2..1ca9d6f 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -3,12 +3,11 @@ package ln import ( - "crypto/aes" - "crypto/cipher" + "bytes" "crypto/rand" "crypto/sha256" "encoding/base64" - "encoding/json" + "encoding/gob" "fmt" "io" "log" @@ -30,7 +29,7 @@ import ( // maximum number of participants in ClaimJoin const ( - maxParties = 5 + maxParties = 10 PeginBlocks = 10 //102 ) @@ -63,7 +62,7 @@ type Coordination struct { // human readable status of the claimjoin Status string // partially signed elements transaction - PSET string + PSET []byte } type ClaimParty struct { @@ -117,12 +116,12 @@ func OnTimer() { } defer clean() - if GetBlockHeight(cl) < ClaimBlockHeight { - // not yet matured + if GetBlockHeight(cl) < ClaimBlockHeight-1 { + // start signing process one block before maturity return } - startingFee := 36*len(ClaimParties) - 1 + startingFee := 36 * len(ClaimParties) if claimPSET == "" { var err error @@ -146,12 +145,12 @@ func OnTimer() { for i, output := range analyzed.Outputs { if output.Blind && output.Status == "unblinded" { blinder := decoded.Outputs[i].BlinderIndex - ClaimStatus = "Blinding " + strconv.Itoa(i+1) + "/" + strconv.Itoa(len(ClaimParties)) - - log.Println(ClaimStatus) + total := strconv.Itoa(len(ClaimParties)) + ClaimStatus = "Blinding " + strconv.Itoa(i+1) + "/" + total if blinder == 0 { // my output + log.Println(ClaimStatus) claimPSET, _, err = liquid.ProcessPSET(claimPSET, config.Config.ElementsWallet) if err != nil { log.Println("Unable to blind output, cancelling ClaimJoin:", err) @@ -163,13 +162,21 @@ func OnTimer() { if i == len(ClaimParties)-1 { // the final blinder can blind and sign at once action = "process2" - ClaimStatus = "Signing " + strconv.Itoa(len(ClaimParties)) + "/" + strconv.Itoa(len(ClaimParties)) + ClaimStatus += " & Signing 1/" + total } if checkPeerStatus(blinder) { + serializedPset, err := base64.StdEncoding.DecodeString(claimPSET) + if err != nil { + log.Println("Unable to serialize PSET") + return + } + + log.Println(ClaimStatus) + if !SendCoordination(ClaimParties[blinder].PubKey, &Coordination{ Action: action, - PSET: claimPSET, + PSET: serializedPset, Status: ClaimStatus, ClaimBlockHeight: ClaimBlockHeight, }) { @@ -177,7 +184,6 @@ func OnTimer() { EndClaimJoin("", "Coordination failure") } } - return } } @@ -188,7 +194,6 @@ func OnTimer() { input := decoded.Inputs[i] if len(input.FinalScriptWitness) == 0 { ClaimStatus = "Signing " + strconv.Itoa(len(ClaimParties)-i) + "/" + strconv.Itoa(len(ClaimParties)) - log.Println(ClaimStatus) if i == 0 { @@ -201,9 +206,15 @@ func OnTimer() { } } else { if checkPeerStatus(i) { + serializedPset, err := base64.StdEncoding.DecodeString(claimPSET) + if err != nil { + log.Println("Unable to serialize PSET") + return + } + if !SendCoordination(ClaimParties[i].PubKey, &Coordination{ Action: "process", - PSET: claimPSET, + PSET: serializedPset, Status: ClaimStatus, ClaimBlockHeight: ClaimBlockHeight, }) { @@ -267,7 +278,7 @@ func OnTimer() { db.Save("ClaimJoin", "claimPSET", &claimPSET) db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) - } else { + } else if GetBlockHeight(cl) >= ClaimBlockHeight { // send raw transaction txId, err := liquid.SendRawTransaction(rawHex) if err != nil { @@ -331,86 +342,87 @@ func Broadcast(fromNodeId string, message *Message) error { } } - if fromNodeId != myNodeId && (message.Asset == "pegin_started" && keyToNodeId[message.Sender] != "" || message.Asset == "pegin_ended" && PeginHandler == "") { - // has been previously received from upstream, ignore - return nil - } + if fromNodeId == myNodeId || (fromNodeId != myNodeId && (message.Asset == "pegin_started" && keyToNodeId[message.Sender] == "" || message.Asset == "pegin_ended" && PeginHandler != "")) { + // forward to everyone else - res, err := ps.ListPeers(client) - if err != nil { - return err - } - - cl, clean, er := GetClient() - if er != nil { - return err - } - defer clean() - - for _, peer := range res.GetPeers() { - // don't send back to where it came from - if peer.NodeId != fromNodeId { - SendCustomMessage(cl, peer.NodeId, message) + res, err := ps.ListPeers(client) + if err != nil { + return err } - } - if fromNodeId == myNodeId { - // do nothing more after sending the original broadcast - return nil - } - - if keyToNodeId[message.Sender] != fromNodeId { - // store for relaying further encrypted messages - keyToNodeId[message.Sender] = fromNodeId - db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) - } + cl, clean, er := GetClient() + if er != nil { + return err + } + defer clean() - switch message.Asset { - case "pegin_started": - if MyRole == "initiator" { - // two simultaneous initiators conflict - if len(ClaimParties) < 2 { - MyRole = "none" - db.Save("ClaimJoin", "MyRole", MyRole) - log.Println("Initiator collision, switching to none") - } else { - log.Println("Initiator collision, staying as initiator") - break + for _, peer := range res.GetPeers() { + // don't send it back to where it came from + if peer.NodeId != fromNodeId { + SendCustomMessage(cl, peer.NodeId, message) } - } else if MyRole == "joiner" { - // already joined another group, ignore + } + + if fromNodeId == myNodeId { + // do nothing more if this is my own broadcast return nil } - // where to forward claimjoin request - PeginHandler = message.Sender - // Time limit to apply is communicated via Amount - ClaimBlockHeight = uint32(message.Amount) - ClaimStatus = "Received invitation to ClaimJoin" - log.Println(ClaimStatus) + // react to received broadcast + switch message.Asset { + case "pegin_started": + if MyRole == "initiator" { + // two simultaneous initiators conflict + if len(ClaimParties) < 2 { + MyRole = "none" + db.Save("ClaimJoin", "MyRole", MyRole) + log.Println("Initiator collision, switching to none") + } else { + log.Println("Initiator collision, staying as initiator") + break + } + } else if MyRole == "joiner" { + // already joined another group, ignore + return nil + } - // persist to db - db.Save("ClaimJoin", "PeginHandler", PeginHandler) - db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) - db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - - case "pegin_ended": - // only trust the message from the original handler - if MyRole == "joiner" && PeginHandler == message.Sender { - txId := message.Payload - if txId != "" { - ClaimStatus = "ClaimJoin pegin successful! Liquid TxId: " + txId + // where to forward claimjoin request + PeginHandler = message.Sender + // Time limit to apply is communicated via Amount + ClaimBlockHeight = uint32(message.Amount) + ClaimStatus = "Received invitation to ClaimJoin" + log.Println(ClaimStatus) + + // persist to db + db.Save("ClaimJoin", "PeginHandler", PeginHandler) + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + + case "pegin_ended": + // only trust the message from the original handler + if MyRole == "joiner" && PeginHandler == message.Sender && config.Config.PeginClaimScript != "done" { + txId := string(message.Payload) + if txId != "" { + ClaimStatus = "ClaimJoin pegin successful! Liquid TxId: " + txId + log.Println(ClaimStatus) + // signal to telegram bot + config.Config.PeginTxId = txId + config.Config.PeginClaimScript = "done" + } else { + ClaimStatus = "Invitation to ClaimJoin revoked" + } log.Println(ClaimStatus) - // signal to telegram bot - config.Config.PeginTxId = txId - config.Config.PeginClaimScript = "done" - } else { - ClaimStatus = "Invitations to ClaimJoin revoked" + resetClaimJoin() } - log.Println(ClaimStatus) - resetClaimJoin() } } + + if keyToNodeId[message.Sender] != fromNodeId { + // store for relaying further encrypted messages + keyToNodeId[message.Sender] = fromNodeId + db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) + } + return nil } @@ -419,19 +431,22 @@ func Broadcast(fromNodeId string, message *Message) error { // Peers track sources of encrypted messages to forward back the replies func SendCoordination(destinationPubKey string, message *Coordination) bool { - payload, err := json.Marshal(message) - if err != nil { - return false - } - destinationNodeId := keyToNodeId[destinationPubKey] if destinationNodeId == "" { log.Println("Cannot send coordination, destination PubKey has no matching NodeId") return false } + // Serialize the message using gob + var buffer bytes.Buffer + encoder := gob.NewEncoder(&buffer) + if err := encoder.Encode(message); err != nil { + log.Println("Cannot encode with GOB:", err) + return false + } + // Encrypt the message using the base64 receiver's public key - ciphertext, err := eciesEncrypt(destinationPubKey, payload) + ciphertext, err := eciesEncrypt(destinationPubKey, buffer.Bytes()) if err != nil { log.Println("Error encrypting message:", err) return false @@ -476,15 +491,8 @@ func Process(message *Message, senderNodeId string) { // log.Println("My pubKey:", myPublicKey()) if message.Destination == myPublicKey() { - // Convert to []byte - payload, err := base64.StdEncoding.DecodeString(message.Payload) - if err != nil { - log.Printf("Error decoding payload: %s", err) - return - } - // Decrypt the message using my private key - plaintext, err := eciesDecrypt(myPrivateKey, payload) + plaintext, err := eciesDecrypt(myPrivateKey, message.Payload) if err != nil { log.Printf("Error decrypting payload: %s", err) return @@ -492,13 +500,19 @@ func Process(message *Message, senderNodeId string) { // recover the struct var msg Coordination - err = json.Unmarshal(plaintext, &msg) - if err != nil { + var buffer bytes.Buffer + + // Write the byte slice into the buffer + buffer.Write(plaintext) + + // Deserialize binary data + decoder := gob.NewDecoder(&buffer) + if err := decoder.Decode(&msg); err != nil { log.Printf("Received an incorrectly formed Coordination: %s", err) return } - claimPSET = msg.PSET + claimPSET = base64.StdEncoding.EncodeToString(msg.PSET) switch msg.Action { case "add": @@ -518,6 +532,17 @@ func Process(message *Message, senderNodeId string) { ClaimStatus = "Added new joiner, total participants: " + strconv.Itoa(len(ClaimParties)) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) log.Println(ClaimStatus) + + if len(ClaimParties) > 2 { + // inform the other joiners of the new ClaimBlockHeight + for i := 1; i < len(ClaimParties)-2; i++ { + SendCoordination(ClaimParties[i].PubKey, &Coordination{ + Action: "confirm_add", + ClaimBlockHeight: ClaimBlockHeight, + Status: "Another peer joined, total participants: " + strconv.Itoa(len(ClaimParties)), + }) + } + } } } else { if SendCoordination(msg.Joiner.PubKey, &Coordination{ @@ -556,6 +581,7 @@ func Process(message *Message, senderNodeId string) { // persist to db db.Save("ClaimJoin", "MyRole", MyRole) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) case "refuse_add": log.Println(msg.Status) @@ -650,10 +676,16 @@ func Process(message *Message, senderNodeId string) { db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + serializedPset, err := base64.StdEncoding.DecodeString(claimPSET) + if err != nil { + log.Println("Unable to serialize PSET") + return + } + // return PSET to Handler if !SendCoordination(PeginHandler, &Coordination{ Action: "process", - PSET: claimPSET, + PSET: serializedPset, }) { log.Println("Unable to send blind coordination, cancelling ClaimJoin") EndClaimJoin("", "Coordination failure") @@ -731,7 +763,7 @@ func EndClaimJoin(txId string, status string) bool { Asset: "pegin_ended", Amount: uint64(0), Sender: myPublicKey(), - Payload: txId, + Payload: []byte(txId), Destination: status, }) == nil { if txId != "" { @@ -887,7 +919,7 @@ func addClaimParty(newParty *ClaimParty) (bool, string) { // persist to db db.Save("ClaimJoin", "claimParties", ClaimParties) - return true, "Successfully joined as participant #" + strconv.Itoa(len(ClaimParties)) + return true, "Successfully joined, total participants: " + strconv.Itoa(len(ClaimParties)) } // remove claim party from the list by public key @@ -938,50 +970,12 @@ func myPublicKey() string { return publicKeyToBase64(myPrivateKey.PubKey()) } -// Payload is encrypted with a symmetric AES key and returned as base64 string -// The AES key is encrypted using destination secp256k1 pubKey, returned as base64 -func encryptPayload(pubKeyString string, plaintext []byte) (string, string, error) { - // Generate AES key - aesKey := make([]byte, 32) // 256-bit key - if _, err := rand.Read(aesKey); err != nil { - return "", "", err - } - - block, err := aes.NewCipher(aesKey) - if err != nil { - return "", "", err - } - - nonce := make([]byte, aes.BlockSize) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return "", "", err - } - - stream := cipher.NewCTR(block, nonce) - ciphertext := make([]byte, len(plaintext)) - stream.XORKeyStream(ciphertext, plaintext) - - // Prepend the nonce to the ciphertext - finalCiphertext := append(nonce, ciphertext...) - - // convert to base64 - base64cypherText := base64.StdEncoding.EncodeToString(finalCiphertext) - - // Encrypt the AES key using RSA public key and returns the encrypted key in base64 - encryptedKey, err := eciesEncrypt(pubKeyString, aesKey) - if err != nil { - return "", "", err - } - - return base64cypherText, encryptedKey, nil -} - // Encrypt with base64 public key -func eciesEncrypt(pubKeyString string, message []byte) (string, error) { +func eciesEncrypt(pubKeyString string, message []byte) ([]byte, error) { pubKey, err := base64ToPublicKey(pubKeyString) if err != nil { - return "", err + return nil, err } ephemeralPrivKey := generatePrivateKey() @@ -990,17 +984,17 @@ func eciesEncrypt(pubKeyString string, message []byte) (string, error) { hkdf := hkdf.New(sha256.New, sharedSecret[:], nil, nil) encryptionKey := make([]byte, chacha20poly1305.KeySize) if _, err := io.ReadFull(hkdf, encryptionKey); err != nil { - return "", err + return nil, err } aead, err := chacha20poly1305.New(encryptionKey) if err != nil { - return "", err + return nil, err } nonce := make([]byte, chacha20poly1305.NonceSize) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return "", err + return nil, err } ciphertext := aead.Seal(nil, nonce, message, nil) @@ -1008,7 +1002,7 @@ func eciesEncrypt(pubKeyString string, message []byte) (string, error) { result := append(ephemeralPrivKey.PubKey().SerializeCompressed(), nonce...) result = append(result, ciphertext...) - return base64.StdEncoding.EncodeToString(result), nil + return result, nil } func eciesDecrypt(privKey *btcec.PrivateKey, ciphertext []byte) ([]byte, error) { diff --git a/cmd/psweb/ln/common.go b/cmd/psweb/ln/common.go index 3e4c261..a6f246c 100644 --- a/cmd/psweb/ln/common.go +++ b/cmd/psweb/ln/common.go @@ -146,17 +146,17 @@ type DataPoint struct { TimeUTC string } -// sent/received as json +// sent/received as GOB type Message struct { // cleartext announcements - Version int `json:"version"` - Memo string `json:"memo"` - Asset string `json:"asset"` - Amount uint64 `json:"amount"` - // for encrypted communications via peer relay - Sender string `json:"sender"` - Destination string `json:"destination"` - Payload string `json:"payload"` + Version int + Memo string + Asset string + Amount uint64 + // encrypted communications via peer relay + Sender string + Destination string + Payload []byte } type BalanceInfo struct { @@ -168,7 +168,7 @@ type BalanceInfo struct { const ( ignoreForwardsMsat = 1_000_000 // one custom type for peeswap web - messageType = uint32(42067) + messageType = uint32(42066) MessageVersion = 1 ) diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 347e84a..534a2ae 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -8,8 +8,8 @@ import ( "crypto/rand" "crypto/sha256" "encoding/base64" + "encoding/gob" "encoding/hex" - "encoding/json" "errors" "fmt" "log" @@ -1191,11 +1191,18 @@ func subscribeMessages(ctx context.Context, client lnrpc.LightningClient) error } if data.Type == messageType { var msg Message - err := json.Unmarshal(data.Data, &msg) - if err != nil { - log.Println("Received an incorrectly formed message") + var buffer bytes.Buffer + + // Write the byte slice into the buffer + buffer.Write(data.Data) + + // Deserialize binary data + decoder := gob.NewDecoder(&buffer) + if err := decoder.Decode(&msg); err != nil { + log.Println("Cannot deserialize the received message ") continue } + if msg.Version != MessageVersion { log.Println("Received a message with wrong version number") continue @@ -1291,17 +1298,17 @@ func SendCustomMessage(client lnrpc.LightningClient, peerId string, message *Mes return err } - data, err := json.Marshal(message) - if err != nil { + // Serialize the message using gob + var buffer bytes.Buffer + encoder := gob.NewEncoder(&buffer) + if err := encoder.Encode(message); err != nil { return err } - log.Println("Message size:", len(data)) - req := &lnrpc.SendCustomMessageRequest{ Peer: peerByte, Type: messageType, - Data: data, + Data: buffer.Bytes(), } _, err = client.SendCustomMessage(context.Background(), req) diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 631b695..5c7cd0e 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -1121,7 +1121,7 @@ func checkPegin() { if ln.MyRole != "none" { // blocks to wait before switching back to indivicual claim - margin := uint32(2) + margin := uint32(20) if ln.MyRole == "initiator" && len(ln.ClaimParties) < 2 { margin = 1 } From 9a8033772d15d4ea92e7d8b0e48e637da1e4ee00 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Thu, 8 Aug 2024 22:38:16 +0200 Subject: [PATCH 14/98] add more logs --- .vscode/launch.json | 2 +- cmd/psweb/ln/claimjoin.go | 73 ++++++++++++++++++++++++--------------- cmd/psweb/ln/cln.go | 1 + cmd/psweb/ln/lnd.go | 4 +-- cmd/psweb/main.go | 20 +++++++---- cmd/psweb/telegram.go | 5 +-- 6 files changed, 67 insertions(+), 38 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 9d86866..2c86c18 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,7 @@ "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - "args": ["-datadir", "/home/vlad/.peerswap2_t4"] + "args": ["-datadir", "/home/vlad/.peerswap_t4"] //"args": ["-datadir", "/home/vlad/.peerswap2"] } ] diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 1ca9d6f..a99cb6c 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -11,7 +11,9 @@ import ( "fmt" "io" "log" + "os" "strconv" + "time" mathRand "math/rand" @@ -99,9 +101,13 @@ func loadClaimJoinDB() { db.Load("ClaimJoin", "claimPSET", &claimPSET) } - var serializedKey []byte - db.Load("ClaimJoin", "serializedPrivateKey", &serializedKey) - myPrivateKey, _ = btcec.PrivKeyFromBytes(serializedKey) + if MyRole != "none" { + var serializedKey []byte + db.Load("ClaimJoin", "serializedPrivateKey", &serializedKey) + myPrivateKey, _ = btcec.PrivKeyFromBytes(serializedKey) + log.Println("Started as ClaimJoin " + MyRole + " with pubKey " + MyPublicKey()) + } + } // runs every minute @@ -375,10 +381,14 @@ func Broadcast(fromNodeId string, message *Message) error { // two simultaneous initiators conflict if len(ClaimParties) < 2 { MyRole = "none" + PeginHandler = "" db.Save("ClaimJoin", "MyRole", MyRole) - log.Println("Initiator collision, switching to none") + db.Save("ClaimJoin", "PeginHandler", PeginHandler) + log.Println("Initiator collision, switching to 'none' and restarting with a random delay") + time.Sleep(time.Duration(mathRand.Intn(60)) * time.Second) + os.Exit(0) } else { - log.Println("Initiator collision, staying as initiator") + log.Println("Initiator collision, staying as initiator because I have joiners") break } } else if MyRole == "joiner" { @@ -461,7 +471,7 @@ func SendCoordination(destinationPubKey string, message *Coordination) bool { return SendCustomMessage(cl, destinationNodeId, &Message{ Version: MessageVersion, Memo: "process", - Sender: myPublicKey(), + Sender: MyPublicKey(), Destination: destinationPubKey, Payload: ciphertext, }) == nil @@ -490,7 +500,7 @@ func Process(message *Message, senderNodeId string) { // log.Println("My pubKey:", myPublicKey()) - if message.Destination == myPublicKey() { + if message.Destination == MyPublicKey() { // Decrypt the message using my private key plaintext, err := eciesDecrypt(myPrivateKey, message.Payload) if err != nil { @@ -521,8 +531,8 @@ func Process(message *Message, senderNodeId string) { return } - if ok, status := addClaimParty(&msg.Joiner); ok { - ClaimBlockHeight = max(ClaimBlockHeight, msg.ClaimBlockHeight) + if ok, newClaimBlockHeight, status := addClaimParty(&msg.Joiner); ok { + ClaimBlockHeight = max(ClaimBlockHeight, newClaimBlockHeight) if SendCoordination(msg.Joiner.PubKey, &Coordination{ Action: "confirm_add", @@ -531,11 +541,11 @@ func Process(message *Message, senderNodeId string) { }) { ClaimStatus = "Added new joiner, total participants: " + strconv.Itoa(len(ClaimParties)) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - log.Println(ClaimStatus) + log.Println("Added "+msg.Joiner.PubKey+", total:", len(ClaimParties)) if len(ClaimParties) > 2 { // inform the other joiners of the new ClaimBlockHeight - for i := 1; i < len(ClaimParties)-2; i++ { + for i := 1; i <= len(ClaimParties)-2; i++ { SendCoordination(ClaimParties[i].PubKey, &Coordination{ Action: "confirm_add", ClaimBlockHeight: ClaimBlockHeight, @@ -729,7 +739,7 @@ func InitiateClaimJoin(claimBlockHeight uint32) bool { Memo: "broadcast", Asset: "pegin_started", Amount: uint64(claimBlockHeight), - Sender: myPublicKey(), + Sender: MyPublicKey(), }) == nil { ClaimBlockHeight = claimBlockHeight party := createClaimParty(claimBlockHeight) @@ -762,7 +772,7 @@ func EndClaimJoin(txId string, status string) bool { Memo: "broadcast", Asset: "pegin_ended", Amount: uint64(0), - Sender: myPublicKey(), + Sender: MyPublicKey(), Payload: []byte(txId), Destination: status, }) == nil { @@ -809,12 +819,15 @@ func JoinClaimJoin(claimBlockHeight uint32) bool { } } - myPrivateKey = generatePrivateKey() - if myPrivateKey != nil { - // persist to db - savePrivateKey() - } else { - return false + // preserve the same key for several join attempts + if myPrivateKey == nil { + myPrivateKey = generatePrivateKey() + if myPrivateKey != nil { + // persist to db + savePrivateKey() + } else { + return false + } } party := createClaimParty(claimBlockHeight) @@ -877,28 +890,28 @@ func createClaimParty(claimBlockHeight uint32) *ClaimParty { return nil } party.Address = res.Address - party.PubKey = myPublicKey() + party.PubKey = MyPublicKey() return party } // add claim party to the list -func addClaimParty(newParty *ClaimParty) (bool, string) { +func addClaimParty(newParty *ClaimParty) (bool, uint32, string) { for _, party := range ClaimParties { if party.ClaimScript == newParty.ClaimScript { // is already in the list - return true, "Was already in the list, total participants: " + strconv.Itoa(len(ClaimParties)) + return true, 0, "Was already in the list, total participants: " + strconv.Itoa(len(ClaimParties)) } } if len(ClaimParties) == maxParties { - return false, "Refused to join, over limit of " + strconv.Itoa(maxParties) + return false, 0, "Refused to join, over limit of " + strconv.Itoa(maxParties) } // verify claimBlockHeight proof, err := bitcoin.GetTxOutProof(newParty.TxId) if err != nil { - return false, "Refused to join, TX not confirmed" + return false, 0, "Refused to join, TX not confirmed" } if proof != newParty.TxoutProof { @@ -911,7 +924,6 @@ func addClaimParty(newParty *ClaimParty) (bool, string) { if txHeight > 0 && claimHight != newParty.ClaimBlockHeight { log.Printf("New joiner's ClaimBlockHeight was wrong: %d vs %d correct", newParty.ClaimBlockHeight, claimHight) - newParty.ClaimBlockHeight = claimHight } ClaimParties = append(ClaimParties, newParty) @@ -919,7 +931,7 @@ func addClaimParty(newParty *ClaimParty) (bool, string) { // persist to db db.Save("ClaimJoin", "claimParties", ClaimParties) - return true, "Successfully joined, total participants: " + strconv.Itoa(len(ClaimParties)) + return true, claimHight, "Successfully joined, total participants: " + strconv.Itoa(len(ClaimParties)) } // remove claim party from the list by public key @@ -966,7 +978,14 @@ func generatePrivateKey() *btcec.PrivateKey { return privKey } -func myPublicKey() string { +func MyPublicKey() string { + if myPrivateKey == nil { + myPrivateKey = generatePrivateKey() + if myPrivateKey != nil { + // persist to db + savePrivateKey() + } + } return publicKeyToBase64(myPrivateKey.PubKey()) } diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index 683d4b9..b824d6c 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -1316,6 +1316,7 @@ func loadClaimJoinDB() {} func OnTimer() {} func InitiateClaimJoin(claimHeight uint32) bool { return false } func JoinClaimJoin(claimHeight uint32) bool { return false } +func MyPublicKey() string { return "" } var ( PeginHandler = "" diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 534a2ae..286428c 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -1226,14 +1226,14 @@ func subscribeMessages(ctx context.Context, client lnrpc.LightningClient) error // received request for information if msg.Memo == "poll" { - if MyRole == "initiator" && myPublicKey() != "" && len(ClaimParties) < maxParties && GetBlockHeight(client) < ClaimBlockHeight { + if MyRole == "initiator" && MyPublicKey() != "" && len(ClaimParties) < maxParties && GetBlockHeight(client) < ClaimBlockHeight { // repeat pegin start info SendCustomMessage(client, nodeId, &Message{ Version: MessageVersion, Memo: "broadcast", Asset: "pegin_started", Amount: uint64(ClaimBlockHeight), - Sender: myPublicKey(), + Sender: MyPublicKey(), }) } diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 5c7cd0e..4484efd 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -380,12 +380,10 @@ func onTimer(firstRun bool) { if !firstRun { // skip first run so that forwards have time to download go ln.ApplyAutoFees() - + // Check if pegin can be claimed, initiated or joined + go checkPegin() } - // Check if pegin can be claimed or joined - go checkPegin() - // advertise Liquid balance go advertiseBalances() @@ -1105,6 +1103,16 @@ func checkPegin() { } if config.Config.PeginTxId == "" { + // send telegram if received new ClaimJoin invitation + if peginInvite != ln.PeginHandler { + t := "🧬 Someone has started a new ClaimJoin pegin" + if ln.PeginHandler == "" { + t = "🧬 ClaimJoin pegin has ended" + } + if telegramSendMessage(t) { + peginInvite = ln.PeginHandler + } + } return } @@ -1194,7 +1202,7 @@ func checkPegin() { // I will coordinate this join if ln.InitiateClaimJoin(claimHeight) { t := "Sent ClaimJoin invitations" - log.Println(t) + log.Println(t + " as " + ln.MyPublicKey()) telegramSendMessage("🧬 " + t) ln.MyRole = "initiator" db.Save("ClaimJoin", "MyRole", ln.MyRole) @@ -1207,7 +1215,7 @@ func checkPegin() { // join by replying to initiator if ln.JoinClaimJoin(claimHeight) { t := "Applied to ClaimJoin group" - log.Println(t) + log.Println(t + " " + ln.PeginHandler + " as " + ln.MyPublicKey()) telegramSendMessage("🧬 " + t) } else { log.Println("Failed to apply to ClaimJoin, continuing as a single pegin") diff --git a/cmd/psweb/telegram.go b/cmd/psweb/telegram.go index 32afdef..00ebae6 100644 --- a/cmd/psweb/telegram.go +++ b/cmd/psweb/telegram.go @@ -18,8 +18,9 @@ import ( ) var ( - chatId int64 - bot *tgbotapi.BotAPI + chatId int64 + bot *tgbotapi.BotAPI + peginInvite string ) func telegramStart() { From d8686b75ec9f49e31c647c1de6dd6c5db1042378 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Thu, 8 Aug 2024 23:11:25 +0200 Subject: [PATCH 15/98] works --- cmd/psweb/ln/claimjoin.go | 22 +++++++++++++++++----- cmd/psweb/ln/cln.go | 3 ++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index a99cb6c..b4f7a5e 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -163,12 +163,14 @@ func OnTimer() { EndClaimJoin("", "Coordination failure") return } + ClaimStatus += " done" + log.Println(ClaimStatus) } else { action := "process" if i == len(ClaimParties)-1 { // the final blinder can blind and sign at once action = "process2" - ClaimStatus += " & Signing 1/" + total + ClaimStatus += " & Signing " + strconv.Itoa(blinder) + "/" + total } if checkPeerStatus(blinder) { @@ -190,6 +192,8 @@ func OnTimer() { EndClaimJoin("", "Coordination failure") } } + + db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) return } } @@ -210,6 +214,8 @@ func OnTimer() { EndClaimJoin("", "Initiator signing failure") return } + ClaimStatus += " done" + log.Println(ClaimStatus) } else { if checkPeerStatus(i) { serializedPset, err := base64.StdEncoding.DecodeString(claimPSET) @@ -228,7 +234,7 @@ func OnTimer() { EndClaimJoin("", "Coordination failure") } } - + db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) return } } @@ -286,6 +292,8 @@ func OnTimer() { db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) } else if GetBlockHeight(cl) >= ClaimBlockHeight { // send raw transaction + log.Println("Posting final TX") + txId, err := liquid.SendRawTransaction(rawHex) if err != nil { if err.Error() == "-27: Transaction already in block chain" { @@ -414,7 +422,6 @@ func Broadcast(fromNodeId string, message *Message) error { txId := string(message.Payload) if txId != "" { ClaimStatus = "ClaimJoin pegin successful! Liquid TxId: " + txId - log.Println(ClaimStatus) // signal to telegram bot config.Config.PeginTxId = txId config.Config.PeginClaimScript = "done" @@ -660,6 +667,11 @@ func Process(message *Message, senderNodeId string) { } } + ClaimStatus = msg.Status + log.Println(ClaimStatus) + + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + // Save received claimPSET, execute OnTimer db.Save("ClaimJoin", "claimPSET", &claimPSET) OnTimer() @@ -679,8 +691,7 @@ func Process(message *Message, senderNodeId string) { } ClaimBlockHeight = msg.ClaimBlockHeight - ClaimStatus = msg.Status - + ClaimStatus = msg.Status + " done" log.Println(ClaimStatus) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) @@ -696,6 +707,7 @@ func Process(message *Message, senderNodeId string) { if !SendCoordination(PeginHandler, &Coordination{ Action: "process", PSET: serializedPset, + Status: ClaimStatus, }) { log.Println("Unable to send blind coordination, cancelling ClaimJoin") EndClaimJoin("", "Coordination failure") diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index b824d6c..2fc10b9 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -1317,13 +1317,14 @@ func OnTimer() {} func InitiateClaimJoin(claimHeight uint32) bool { return false } func JoinClaimJoin(claimHeight uint32) bool { return false } func MyPublicKey() string { return "" } +func EndClaimJoin(a, b string) {} var ( PeginHandler = "" MyRole = "none" ClaimStatus = "" ClaimBlockHeight = uint32(0) - ClaimParties = []int + ClaimParties []int ) const PeginBlocks = 102 From 7ed384e46ef467de5fe00b1c08f8969b1205c87b Mon Sep 17 00:00:00 2001 From: Impa10r Date: Thu, 8 Aug 2024 23:57:26 +0200 Subject: [PATCH 16/98] add fallback --- cmd/psweb/ln/claimjoin.go | 54 ++++++++++++++++++++++++++++++++++++--- cmd/psweb/main.go | 3 ++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index b4f7a5e..db9c265 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -8,6 +8,7 @@ import ( "crypto/sha256" "encoding/base64" "encoding/gob" + "encoding/hex" "fmt" "io" "log" @@ -52,10 +53,12 @@ var ( ClaimParties []*ClaimParty // PSET to be blinded and signed by all parties claimPSET string + // final raw tx that can by published by any joiner if initiator defaults + finalRawTX string ) type Coordination struct { - // possible actions: add, confirm_add, refuse_add, process, process2, remove + // possible actions: add, confirm_add, refuse_add, remove, process, process2, final Action string // new joiner details Joiner ClaimParty @@ -112,7 +115,7 @@ func loadClaimJoinDB() { // runs every minute func OnTimer() { - if !config.Config.PeginClaimJoin || MyRole != "initiator" || len(ClaimParties) < 2 { + if !config.Config.PeginClaimJoin || len(ClaimParties) < 2 { return } @@ -127,6 +130,29 @@ func OnTimer() { return } + // fallback to publish finalized TX if the initiator defaults for 10 blocks + if MyRole == "joiner" && GetBlockHeight(cl) > ClaimBlockHeight+10 && finalRawTX != "" { + log.Println("Posting final TX as a backup") + txId, err := liquid.SendRawTransaction(finalRawTX) + if err != nil { + if err.Error() == "-27: Transaction already in block chain" { + decodedTx, err := liquid.DecodeRawTransaction(finalRawTX) + if err == nil { + txId = decodedTx.Txid + } + } else { + EndClaimJoin("", "Final TX send failure") + return + } + } + EndClaimJoin(txId, "done") + return + } + + if MyRole != "initiator" { + return + } + startingFee := 36 * len(ClaimParties) if claimPSET == "" { @@ -291,7 +317,7 @@ func OnTimer() { db.Save("ClaimJoin", "claimPSET", &claimPSET) db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) } else if GetBlockHeight(cl) >= ClaimBlockHeight { - // send raw transaction + // post raw transaction log.Println("Posting final TX") txId, err := liquid.SendRawTransaction(rawHex) @@ -304,6 +330,22 @@ func OnTimer() { } } EndClaimJoin(txId, "done") + } else { + // Decode the hex string to []byte + bytes, err := hex.DecodeString(rawHex) + if err != nil { + log.Println("Error converting final TX to []byte") + return + } + + // share rawTX with all the participants as a backup + for i := 1; i < len(ClaimParties); i++ { + SendCoordination(ClaimParties[i].PubKey, &Coordination{ + Action: "final", + PSET: bytes, + ClaimBlockHeight: ClaimBlockHeight, + }) + } } } } @@ -712,6 +754,11 @@ func Process(message *Message, senderNodeId string) { log.Println("Unable to send blind coordination, cancelling ClaimJoin") EndClaimJoin("", "Coordination failure") } + + case "final": // hold on to final raw tx + finalRawTX = hex.EncodeToString(msg.PSET) + ClaimStatus = "Received final raw TX as a backup" + log.Println(ClaimStatus) } return } @@ -812,6 +859,7 @@ func resetClaimJoin() { PeginHandler = "" ClaimStatus = "No ClaimJoin peg-in is pending" keyToNodeId = make(map[string]string) + finalRawTX = "" // persist to db db.Save("ClaimJoin", "claimParties", ClaimParties) diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 4484efd..6f5ef3b 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -1128,9 +1128,10 @@ func checkPegin() { } if ln.MyRole != "none" { - // blocks to wait before switching back to indivicual claim + // 20 blocks to wait before switching back to indivicual claim margin := uint32(20) if ln.MyRole == "initiator" && len(ln.ClaimParties) < 2 { + // noone has joined anyway margin = 1 } if currentBlockHeight > ln.ClaimBlockHeight+margin { From 25e1fe585cdd9c00d69d948527e2c3f5fd8c1039 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Sun, 11 Aug 2024 12:17:32 +0200 Subject: [PATCH 17/98] Switch to onBlock --- .vscode/launch.json | 2 +- CHANGELOG.md | 2 +- cmd/psweb/handlers.go | 4 +- cmd/psweb/liquid/elements.go | 42 +++++++-- cmd/psweb/ln/claimjoin.go | 164 ++++++++++++++++++----------------- cmd/psweb/ln/common.go | 2 +- cmd/psweb/ln/lnd.go | 9 ++ cmd/psweb/main.go | 23 ++--- 8 files changed, 143 insertions(+), 105 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2c86c18..9d86866 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,7 @@ "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - "args": ["-datadir", "/home/vlad/.peerswap_t4"] + "args": ["-datadir", "/home/vlad/.peerswap2_t4"] //"args": ["-datadir", "/home/vlad/.peerswap2"] } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index e16c705..c5a12e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## 1.7.0 - Switched Custom Message serialization from JSON to GOB -- Switched Custom Message type from 42067 to 42066 +- Switched Custom Message type from 42067 to 42065 - Better estimate swap-in fee and maximum swap amount - {TODO] Account for circular rebalancing cost and PPM diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index e0409e6..4a2cb97 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -853,9 +853,7 @@ func peginHandler(w http.ResponseWriter, r *http.Request) { } } - var addr liquid.PeginAddress - - err = liquid.GetPeginAddress(&addr) + addr, err := liquid.GetPeginAddress() if err != nil { redirectWithError(w, r, "/bitcoin?", err) return diff --git a/cmd/psweb/liquid/elements.go b/cmd/psweb/liquid/elements.go index cb495f1..f0b4464 100644 --- a/cmd/psweb/liquid/elements.go +++ b/cmd/psweb/liquid/elements.go @@ -322,7 +322,7 @@ type PeginAddress struct { ClaimScript string `json:"claim_script"` } -func GetPeginAddress(address *PeginAddress) error { +func GetPeginAddress() (*PeginAddress, error) { client := ElementsClient() service := &Elements{client} @@ -332,16 +332,17 @@ func GetPeginAddress(address *PeginAddress) error { r, err := service.client.call("getpeginaddress", params, "/wallet/"+wallet) if err = handleError(err, &r); err != nil { log.Printf("getpeginaddress: %v", err) - return err + return nil, err } + var address PeginAddress err = json.Unmarshal([]byte(r.Result), &address) if err != nil { log.Printf("getpeginaddress unmarshall: %v", err) - return err + return nil, err } - return nil + return &address, nil } func ClaimPegin(rawTx, proof, claimScript string) (string, error) { @@ -404,8 +405,8 @@ func EstimateFee() float64 { return math.Round(result.MemPoolMinFee*100_000_000) / 1000 } -// identifies if this version of Elements Core supports discounted vSize -func HasDiscountedvSize() bool { +// get version of Elements Core supports discounted vSize +func GetVersion() float64 { client := ElementsClient() service := &Elements{client} wallet := config.Config.ElementsWallet @@ -414,7 +415,7 @@ func HasDiscountedvSize() bool { r, err := service.client.call("getnetworkinfo", params, "/wallet/"+wallet) if err = handleError(err, &r); err != nil { log.Printf("getnetworkinfo: %v", err) - return false + return 0 } var response map[string]interface{} @@ -422,10 +423,33 @@ func HasDiscountedvSize() bool { err = json.Unmarshal([]byte(r.Result), &response) if err != nil { log.Printf("getnetworkinfo unmarshall: %v", err) - return false + return 0 + } + + return response["version"].(float64) +} + +// returns block hash +func GetBlockHash(block uint32) (string, error) { + client := ElementsClient() + service := &Elements{client} + params := &[]interface{}{block} + + r, err := service.client.call("getblockhash", params, "") + if err = handleError(err, &r); err != nil { + log.Printf("GetBlockHash: %v", err) + return "", err + } + + var response string + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("GetBlockHash unmarshall: %v", err) + return "", err } - return response["version"].(float64) >= 230202 + return response, nil } func CreatePSET(params interface{}) (string, error) { diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index db9c265..f2e990d 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -4,11 +4,11 @@ package ln import ( "bytes" + "context" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/gob" - "encoding/hex" "fmt" "io" "log" @@ -21,6 +21,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/hkdf" + "google.golang.org/grpc" "peerswap-web/cmd/psweb/bitcoin" "peerswap-web/cmd/psweb/config" @@ -28,6 +29,8 @@ import ( "peerswap-web/cmd/psweb/internet" "peerswap-web/cmd/psweb/liquid" "peerswap-web/cmd/psweb/ps" + + "github.com/lightningnetwork/lnd/lnrpc/chainrpc" ) // maximum number of participants in ClaimJoin @@ -53,12 +56,12 @@ var ( ClaimParties []*ClaimParty // PSET to be blinded and signed by all parties claimPSET string - // final raw tx that can by published by any joiner if initiator defaults - finalRawTX string + // count how many times tried to join, to give up after 10 + joinCounter int ) type Coordination struct { - // possible actions: add, confirm_add, refuse_add, remove, process, process2, final + // possible actions: add, confirm_add, refuse_add, remove, process, process2 Action string // new joiner details Joiner ClaimParty @@ -108,56 +111,26 @@ func loadClaimJoinDB() { var serializedKey []byte db.Load("ClaimJoin", "serializedPrivateKey", &serializedKey) myPrivateKey, _ = btcec.PrivKeyFromBytes(serializedKey) - log.Println("Started as ClaimJoin " + MyRole + " with pubKey " + MyPublicKey()) + log.Println("Continue as ClaimJoin " + MyRole + " with pubKey " + MyPublicKey()) } } -// runs every minute -func OnTimer() { - if !config.Config.PeginClaimJoin || len(ClaimParties) < 2 { - return - } - - cl, clean, er := GetClient() - if er != nil { - return - } - defer clean() - - if GetBlockHeight(cl) < ClaimBlockHeight-1 { - // start signing process one block before maturity - return - } - - // fallback to publish finalized TX if the initiator defaults for 10 blocks - if MyRole == "joiner" && GetBlockHeight(cl) > ClaimBlockHeight+10 && finalRawTX != "" { - log.Println("Posting final TX as a backup") - txId, err := liquid.SendRawTransaction(finalRawTX) - if err != nil { - if err.Error() == "-27: Transaction already in block chain" { - decodedTx, err := liquid.DecodeRawTransaction(finalRawTX) - if err == nil { - txId = decodedTx.Txid - } - } else { - EndClaimJoin("", "Final TX send failure") - return - } - } - EndClaimJoin(txId, "done") +// runs every block +func onBlock(blockHeight uint32) { + if !config.Config.PeginClaimJoin || MyRole != "initiator" || len(ClaimParties) < 2 || blockHeight < ClaimBlockHeight { return } - if MyRole != "initiator" { - return - } + errorCounter := 0 + totalFee := 36 * len(ClaimParties) - startingFee := 36 * len(ClaimParties) +create_pset: if claimPSET == "" { var err error - claimPSET, err = createClaimPSET(startingFee) + // start with default fee + claimPSET, err = createClaimPSET(totalFee) if err != nil { return } @@ -174,6 +147,19 @@ func OnTimer() { return } + if len(analyzed.Outputs) != len(ClaimParties)+2 || len(decoded.Inputs) != len(ClaimParties) { + ClaimStatus = "Misformed PSET, trying again" + claimPSET = "" + db.Save("ClaimJoin", "claimPSET", &claimPSET) + db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) + log.Println(ClaimStatus) + errorCounter++ + if errorCounter < 10 { + goto create_pset + } + return + } + for i, output := range analyzed.Outputs { if output.Blind && output.Status == "unblinded" { blinder := decoded.Outputs[i].BlinderIndex @@ -216,6 +202,7 @@ func OnTimer() { }) { log.Println("Unable to send coordination, cancelling ClaimJoin") EndClaimJoin("", "Coordination failure") + return } } @@ -242,6 +229,7 @@ func OnTimer() { } ClaimStatus += " done" log.Println(ClaimStatus) + db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) } else { if checkPeerStatus(i) { serializedPset, err := base64.StdEncoding.DecodeString(claimPSET) @@ -266,6 +254,12 @@ func OnTimer() { } } + // analyze again after I sign + analyzed, err = liquid.AnalyzePSET(claimPSET) + if err != nil { + return + } + if analyzed.Next == "extractor" { // finalize and check fee rawHex, done, err := liquid.FinalizePSET(claimPSET) @@ -304,19 +298,19 @@ func OnTimer() { } if feeValue != exactFee { - log.Printf("Paid fee: %d, required fee: %d, starting over", feeValue, exactFee) // start over with the exact fee - claimPSET, err = createClaimPSET(exactFee) - if err != nil { - return - } + totalFee = exactFee ClaimStatus = "Redo to improve fee" + claimPSET = "" db.Save("ClaimJoin", "claimPSET", &claimPSET) db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) - } else if GetBlockHeight(cl) >= ClaimBlockHeight { + + goto create_pset + + } else { // post raw transaction log.Println("Posting final TX") @@ -330,29 +324,13 @@ func OnTimer() { } } EndClaimJoin(txId, "done") - } else { - // Decode the hex string to []byte - bytes, err := hex.DecodeString(rawHex) - if err != nil { - log.Println("Error converting final TX to []byte") - return - } - - // share rawTX with all the participants as a backup - for i := 1; i < len(ClaimParties); i++ { - SendCoordination(ClaimParties[i].PubKey, &Coordination{ - Action: "final", - PSET: bytes, - ClaimBlockHeight: ClaimBlockHeight, - }) - } } } } func checkPeerStatus(i int) bool { ClaimParties[i].SentCount++ - if ClaimParties[i].SentCount > 10 { + if ClaimParties[i].SentCount > 3 { // peer is offline, kick him kickPeer(ClaimParties[i].PubKey, "being unresponsive") return false @@ -451,6 +429,9 @@ func Broadcast(fromNodeId string, message *Message) error { // Time limit to apply is communicated via Amount ClaimBlockHeight = uint32(message.Amount) ClaimStatus = "Received invitation to ClaimJoin" + // reset counter of join attempts + joinCounter = 0 + log.Println(ClaimStatus) // persist to db @@ -547,8 +528,6 @@ func Process(message *Message, senderNodeId string) { db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) } - // log.Println("My pubKey:", myPublicKey()) - if message.Destination == MyPublicKey() { // Decrypt the message using my private key plaintext, err := eciesDecrypt(myPrivateKey, message.Payload) @@ -714,9 +693,9 @@ func Process(message *Message, senderNodeId string) { db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - // Save received claimPSET, execute OnTimer + // Save received claimPSET, execute onBlock to continue signing db.Save("ClaimJoin", "claimPSET", &claimPSET) - OnTimer() + onBlock(ClaimBlockHeight) return } @@ -754,11 +733,6 @@ func Process(message *Message, senderNodeId string) { log.Println("Unable to send blind coordination, cancelling ClaimJoin") EndClaimJoin("", "Coordination failure") } - - case "final": // hold on to final raw tx - finalRawTX = hex.EncodeToString(msg.PSET) - ClaimStatus = "Received final raw TX as a backup" - log.Println(ClaimStatus) } return } @@ -840,10 +814,6 @@ func EndClaimJoin(txId string, status string) bool { // signal to telegram bot config.Config.PeginTxId = txId config.Config.PeginClaimScript = "done" - } else { - log.Println("ClaimJoin failed. Switching back to individual.") - config.Config.PeginClaimJoin = false - config.Save() } resetClaimJoin() return true @@ -859,7 +829,6 @@ func resetClaimJoin() { PeginHandler = "" ClaimStatus = "No ClaimJoin peg-in is pending" keyToNodeId = make(map[string]string) - finalRawTX = "" // persist to db db.Save("ClaimJoin", "claimParties", ClaimParties) @@ -872,6 +841,15 @@ func resetClaimJoin() { // called for ClaimJoin joiner candidate after his pegin funding tx confirms func JoinClaimJoin(claimBlockHeight uint32) bool { + if joinCounter > 3 { + ClaimStatus = "Initator does not respond, forget him" + log.Println(ClaimStatus) + PeginHandler = "" + db.Save("ClaimJoin", "PeginHandler", PeginHandler) + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + return true + } + if myNodeId == "" { // populates myNodeId if getLndVersion() == 0 { @@ -897,6 +875,8 @@ func JoinClaimJoin(claimBlockHeight uint32) bool { Joiner: *party, ClaimBlockHeight: claimBlockHeight, }) { + // increment counter + joinCounter++ // initiate array of claim parties for single entry ClaimParties = nil ClaimParties = append(ClaimParties, party) @@ -984,6 +964,9 @@ func addClaimParty(newParty *ClaimParty) (bool, uint32, string) { if txHeight > 0 && claimHight != newParty.ClaimBlockHeight { log.Printf("New joiner's ClaimBlockHeight was wrong: %d vs %d correct", newParty.ClaimBlockHeight, claimHight) + } else { + // cannot verify, have to trust the joiner + claimHight = newParty.ClaimBlockHeight } ClaimParties = append(ClaimParties, newParty) @@ -1220,3 +1203,24 @@ func verifyPSET() bool { return false } + +func subscribeBlocks(conn *grpc.ClientConn) error { + + client := chainrpc.NewChainNotifierClient(conn) + ctx := context.Background() + stream, err := client.RegisterBlockEpochNtfn(ctx, &chainrpc.BlockEpoch{}) + if err != nil { + return err + } + + log.Println("Subscribed to blocks") + + for { + blockEpoch, err := stream.Recv() + if err != nil { + return err + } + + onBlock(blockEpoch.Height) + } +} diff --git a/cmd/psweb/ln/common.go b/cmd/psweb/ln/common.go index a6f246c..790d63a 100644 --- a/cmd/psweb/ln/common.go +++ b/cmd/psweb/ln/common.go @@ -168,7 +168,7 @@ type BalanceInfo struct { const ( ignoreForwardsMsat = 1_000_000 // one custom type for peeswap web - messageType = uint32(42066) + messageType = uint32(42065) MessageVersion = 1 ) diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 286428c..b0e29eb 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -1115,6 +1115,15 @@ func SubscribeAll() { } }() + // subscribe to chain blocks + go func() { + for { + if subscribeBlocks(conn) != nil { + time.Sleep(60 * time.Second) + } + } + }() + // subscribe to Invoices for { if subscribeInvoices(ctx, client) != nil { diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 6f5ef3b..ff7ed8b 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -129,7 +129,13 @@ func main() { } defer cleanup() - hasDiscountedvSize = liquid.HasDiscountedvSize() + // identify if Elements Core supports CT discounts + hasDiscountedvSize = liquid.GetVersion() >= 230202 + // identify if liquid blockchain supports CT discounts + liquidGenesisHash, err := liquid.GetBlockHash(0) + if err == nil { + hasDiscountedvSize = stringIsInSlice(liquidGenesisHash, []string{"6d955c95af04f1d14f5a6e6bd501508b37bddb6202aac6d99d0522a8cb7deef5"}) + } // Load persisted data from database ln.LoadDB() @@ -348,9 +354,6 @@ func onTimer(firstRun bool) { // Back up to Telegram if Liquid balance changed liquidBackup(false) - // Handle ClaimJoin - go ln.OnTimer() - // check for updates go func() { t := internet.GetLatestTag() @@ -1128,15 +1131,15 @@ func checkPegin() { } if ln.MyRole != "none" { - // 20 blocks to wait before switching back to indivicual claim - margin := uint32(20) + // 10 blocks to wait before switching back to individual claim + margin := uint32(10) if ln.MyRole == "initiator" && len(ln.ClaimParties) < 2 { - // noone has joined anyway + // if no one has joined, switch after 1 extra block margin = 1 } if currentBlockHeight > ln.ClaimBlockHeight+margin { // claim pegin individually - t := "ClaimJoin matured, switching to individual claim" + t := "ClaimJoin failed, switching to individual claim" log.Println(t) telegramSendMessage("🧬 " + t) ln.MyRole = "none" @@ -1872,10 +1875,10 @@ func pollBalances() { defer clean() for _, peer := range res.GetPeers() { - if ln.SendCustomMessage(cl, peer.NodeId, &ln.Message{ + if err := ln.SendCustomMessage(cl, peer.NodeId, &ln.Message{ Version: ln.MessageVersion, Memo: "poll", - }) != nil { + }); err != nil { log.Println("Failed to poll balances from", getNodeAlias(peer.NodeId), err) } else { initalPollComplete = true From a07177e9076bf3813194130485e85225eafb2ef9 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Tue, 13 Aug 2024 19:40:38 +0200 Subject: [PATCH 18/98] Fix join block height --- cmd/psweb/ln/claimjoin.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index f2e990d..1eeb229 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -126,10 +126,8 @@ func onBlock(blockHeight uint32) { totalFee := 36 * len(ClaimParties) create_pset: - if claimPSET == "" { var err error - // start with default fee claimPSET, err = createClaimPSET(totalFee) if err != nil { return From 878615337a1bdc0f2cf7301081a642260493e11b Mon Sep 17 00:00:00 2001 From: Impa10r Date: Tue, 13 Aug 2024 19:42:12 +0200 Subject: [PATCH 19/98] Fix join claim height --- .vscode/launch.json | 2 +- CHANGELOG.md | 26 +- README.md | 6 +- SECURITY.md | 2 +- cmd/psweb/handlers.go | 41 +-- cmd/psweb/ln/claimjoin.go | 402 +++++++++++++++++----------- cmd/psweb/ln/cln.go | 7 +- cmd/psweb/ln/lnd.go | 34 +-- cmd/psweb/main.go | 49 ++-- cmd/psweb/telegram.go | 6 +- cmd/psweb/templates/bitcoin.gohtml | 40 +-- cmd/psweb/templates/homepage.gohtml | 2 +- 12 files changed, 367 insertions(+), 250 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 9d86866..2c86c18 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,7 @@ "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - "args": ["-datadir", "/home/vlad/.peerswap2_t4"] + "args": ["-datadir", "/home/vlad/.peerswap_t4"] //"args": ["-datadir", "/home/vlad/.peerswap2"] } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index c5a12e6..fb1c897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ - Fix AutoFees stop working bug - Fix AutoFees applied on startup before forwards history has been downloaded - LND 0.18: Fix AutoFees reacting to temporary balance increase due to pendinng HTLCs -- Highlight outputs to be used for peg-in or BTC withdrawal +- Highlight outputs to be used for pegin or BTC withdrawal - Warn when BTC swap-in amount is unlikely to cover chain fee - Limit L-BTC swap-in amount to avoid excessive fee rate @@ -189,7 +189,7 @@ ## 1.3.5 -- Estimate peg-in transaction size, total fee and PPM +- Estimate pegin transaction size, total fee and PPM - Add peer fee revenue stats to the main page ## 1.3.4 @@ -200,7 +200,7 @@ - Display current fee rates for channels - Add help tooltips - CLN: implement incremental forwards history polling -- LND 0.18+: exact fee when sending change-less peg-in tx (there was a bug in LND below 0.18) +- LND 0.18+: exact fee when sending change-less pegin tx (there was a bug in LND below 0.18) ## 1.3.3 @@ -209,15 +209,15 @@ ## 1.3.2 -- Add Bitcoin UTXOs selection for Liquid Peg-ins -- Allow deducting peg-in chain fee from the amount to avoid change output -- CLN: Can bump peg-in chain fees with RBF -- LND 0.18+: Can bump peg-in chain fees with RBF -- LND below 0.18: Can bump peg-in chain fees with CPFP +- Add Bitcoin UTXOs selection for Liquid Pegins +- Allow deducting pegin chain fee from the amount to avoid change output +- CLN: Can bump pegin chain fees with RBF +- LND 0.18+: Can bump pegin chain fees with RBF +- LND below 0.18: Can bump pegin chain fees with CPFP ## 1.3.1 -- Enable peg-in transaction fee bumping for CLN +- Enable pegin transaction fee bumping for CLN - Add LND log link - Various bug fixes @@ -232,7 +232,7 @@ ## 1.2.5 -- Bug fix for peg-in claim not working in v1.2.4 +- Bug fix for pegin claim not working in v1.2.4 ## 1.2.4 @@ -242,8 +242,8 @@ ## 1.2.3 -- Add fee rate estimate for peg-in -- Allow fee bumping peg-in tx (first CPFP, then RBF) +- Add fee rate estimate for pegin +- Allow fee bumping pegin tx (first CPFP, then RBF) ## 1.2.2 @@ -259,7 +259,7 @@ ## 1.2.0 - Add Bitcoin balance and utxo list display -- Implement Liquid Peg-In functionality +- Implement Liquid Pegin functionality - Fallback to getblock.io if local Bitcoin Core unreachable ## 1.1.8 diff --git a/README.md b/README.md index d7f7adc..eebafee 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # PeerSwap Web UI -A lightweight server-side rendered Web UI for PeerSwap, which allows trustless p2p submarine swaps Lightning<->BTC and Lightning<->Liquid. Also facilitates BTC->Liquid peg-ins and automatic channel fee management. PeerSwap with [Liquid](https://help.blockstream.com/hc/en-us/articles/900001408623-How-does-Liquid-Bitcoin-L-BTC-work) is a great cost efficient way to [rebalance lightning channels](https://medium.com/@goryachev/liquid-rebalancing-of-lightning-channels-2dadf4b2397a). +A lightweight server-side rendered Web UI for PeerSwap, which allows trustless p2p submarine swaps Lightning<->BTC and Lightning<->Liquid. Also facilitates BTC->Liquid pegins and automatic channel fee management. PeerSwap with [Liquid](https://help.blockstream.com/hc/en-us/articles/900001408623-How-does-Liquid-Bitcoin-L-BTC-work) is a great cost efficient way to [rebalance lightning channels](https://medium.com/@goryachev/liquid-rebalancing-of-lightning-channels-2dadf4b2397a). ### Disclaimer @@ -137,7 +137,7 @@ sudo systemctl restart psweb ## Automatic Liquid Swap-Ins -Liquid BTC is more custodial than Bitcoin and Lightning. We do not advise accumulating large balances for long-term holding. Once you gained Liquid in a peer swap-in or a peg-in process, it is better to initiate own swap in to rebalance a channel of your choice. +Liquid BTC is more custodial than Bitcoin and Lightning. We do not advise accumulating large balances for long-term holding. Once you gained Liquid in a peer swap-in or a pegin process, it is better to initiate own swap in to rebalance a channel of your choice. Currently, it is not possible to prevent swap outs by other peers while allowing receipt of swap ins. You don't want your Liquid balance taken, because such a rebalancing may not be optimal for you (but optimal for your peer). @@ -190,7 +190,7 @@ sudo systemctl stop psweb sudo systemctl disable psweb ``` -# Liquid Peg-In +# Liquid Pegin Update: From v1.2.0 this is handled via UI on the Bitcoin page. diff --git a/SECURITY.md b/SECURITY.md index 6315145..62bb00a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,7 +8,7 @@ For networks with small attack surfaces it is possible to opt-in for a less secu There is no centralized server. PeerSwap Web UI does not share your private data with the contributors. The software, however, may utilize API endpoints of github.com, mempool.space, telegram.org and getblock.io to send and receive certain information. You can avoid leaking your IP address to these websites by specifying a Tor proxy on the Configuration page. You may also provide URL of a locally installed Mempool server. -Getblock runs a publicly available Bitcoin Core server. It is used as a fallback when your local installation is not accessible via API or is not configured to enable Liquid peg-ins. The default account is anonymous, but some contributors may have access to monitor usage statistics. You may opt out by registering your own free account at getblock.io and providing its endpoint, or by running your local suitably configured Bitcoin Core software. +Getblock runs a publicly available Bitcoin Core server. It is used as a fallback when your local installation is not accessible via API or is not configured to enable Liquid pegins. The default account is anonymous, but some contributors may have access to monitor usage statistics. You may opt out by registering your own free account at getblock.io and providing its endpoint, or by running your local suitably configured Bitcoin Core software. ## Reporting a Vulnerability diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index 4a2cb97..a63d1f2 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -670,6 +670,7 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { IsClaimJoin bool ClaimJoinStatus string HasClaimJoinPending bool + ClaimJoinETA int } btcBalance := ln.ConfirmedWalletBalance(cl) @@ -701,12 +702,15 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { duration := time.Duration(10*(ln.PeginBlocks-confs)) * time.Minute maxConfs := int32(ln.PeginBlocks) + cjETA := 34 - if config.Config.PeginClaimJoin && ln.MyRole != "none" { - bh := int32(ln.GetBlockHeight(cl)) + bh := int32(ln.GetBlockHeight(cl)) + if ln.MyRole != "none" { target := int32(ln.ClaimBlockHeight) maxConfs = target - bh + confs duration = time.Duration(10*(target-bh)) * time.Minute + } else { + cjETA = int((int32(ln.JoinBlockHeight) - bh + ln.PeginBlocks) / 6) } progress := confs * 100 / int32(maxConfs) @@ -745,7 +749,8 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { CanClaimJoin: hasDiscountedvSize && ln.Implementation == "LND", IsClaimJoin: config.Config.PeginClaimJoin, ClaimJoinStatus: ln.ClaimStatus, - HasClaimJoinPending: ln.PeginHandler != "", + HasClaimJoinPending: ln.ClaimJoinHandler != "", + ClaimJoinETA: cjETA, } // executing template named "bitcoin" @@ -866,7 +871,7 @@ func peginHandler(w http.ResponseWriter, r *http.Request) { claimScript = "" } - label := "Liquid Peg-in" + label := "Liquid Pegin" if !isPegin { label = "BTC Withdrawal" } @@ -878,10 +883,20 @@ func peginHandler(w http.ResponseWriter, r *http.Request) { } if isPegin { - log.Println("New Peg-in TxId:", res.TxId, "RawHex:", res.RawHex, "Claim script:", claimScript) + log.Println("New Pegin TxId:", res.TxId, "RawHex:", res.RawHex, "Claim script:", claimScript) duration := time.Duration(10*ln.PeginBlocks) * time.Minute formattedDuration := time.Time{}.Add(duration).Format("15h 04m") - telegramSendMessage("⏰ Started peg-in " + formatWithThousandSeparators(uint64(res.AmountSat)) + " sats. Time left: " + formattedDuration + ". TxId: `" + res.TxId + "`") + telegramSendMessage("⏰ Started pegin " + formatWithThousandSeparators(uint64(res.AmountSat)) + " sats. Time left: " + formattedDuration + ". TxId: `" + res.TxId + "`") + + if hasDiscountedvSize && ln.Implementation == "LND" { + config.Config.PeginClaimJoin = r.FormValue("claimJoin") == "on" + if config.Config.PeginClaimJoin { + ln.ClaimStatus = "Awaiting tx confirmation" + db.Save("ClaimJoin", "ClaimStatus", ln.ClaimStatus) + } + } else { + config.Config.PeginClaimJoin = false + } } else { log.Println("BTC withdrawal pending, TxId:", res.TxId, "RawHex:", res.RawHex) telegramSendMessage("BTC withdrawal pending: " + formatWithThousandSeparators(uint64(res.AmountSat)) + " sats. TxId: `" + res.TxId + "`") @@ -894,16 +909,6 @@ func peginHandler(w http.ResponseWriter, r *http.Request) { config.Config.PeginReplacedTxId = "" config.Config.PeginFeeRate = uint32(fee) - if hasDiscountedvSize && ln.Implementation == "LND" { - config.Config.PeginClaimJoin = r.FormValue("claimJoin") == "on" - if config.Config.PeginClaimJoin { - ln.ClaimStatus = "Awaiting tx confirmation" - db.Save("ClaimJoin", "ClaimStatus", ln.ClaimStatus) - } - } else { - config.Config.PeginClaimJoin = false - } - if err := config.Save(); err != nil { redirectWithError(w, r, "/bitcoin?", err) return @@ -931,7 +936,7 @@ func bumpfeeHandler(w http.ResponseWriter, r *http.Request) { } if config.Config.PeginTxId == "" { - redirectWithError(w, r, "/bitcoin?", errors.New("no pending peg-in")) + redirectWithError(w, r, "/bitcoin?", errors.New("no pending pegin")) return } @@ -949,7 +954,7 @@ func bumpfeeHandler(w http.ResponseWriter, r *http.Request) { return } - label := "Liquid Peg-in" + label := "Liquid Pegin" if config.Config.PeginClaimScript == "" { label = "BTC Withdrawal" } diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 1eeb229..0657e73 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -45,11 +45,13 @@ var ( // maps learned public keys to node Id keyToNodeId = make(map[string]string) // public key of the sender of pegin_started broadcast - PeginHandler string + ClaimJoinHandler string // when currently pending pegin can be claimed ClaimBlockHeight uint32 + // time limit to join another claim + JoinBlockHeight uint32 // human readable status of the claimjoin - ClaimStatus = "No ClaimJoin peg-in is pending" + ClaimStatus = "No ClaimJoin pegin is pending" // none, initiator or joiner MyRole = "none" // array of initiator + joiners, for initiator only @@ -58,6 +60,8 @@ var ( claimPSET string // count how many times tried to join, to give up after 10 joinCounter int + // flag that PSWeb is about to restart + restarting = false ) type Coordination struct { @@ -95,16 +99,20 @@ type ClaimParty struct { // runs after restart, to continue if pegin is ongoing func loadClaimJoinDB() { - db.Load("ClaimJoin", "PeginHandler", &PeginHandler) + db.Load("ClaimJoin", "ClaimJoinHandler", &ClaimJoinHandler) db.Load("ClaimJoin", "ClaimBlockHeight", &ClaimBlockHeight) + db.Load("ClaimJoin", "JoinBlockHeight", &JoinBlockHeight) db.Load("ClaimJoin", "ClaimStatus", &ClaimStatus) db.Load("ClaimJoin", "MyRole", &MyRole) db.Load("ClaimJoin", "keyToNodeId", &keyToNodeId) - db.Load("ClaimJoin", "claimParties", &ClaimParties) if MyRole == "initiator" { db.Load("ClaimJoin", "claimPSET", &claimPSET) + db.Load("ClaimJoin", "claimParties", &ClaimParties) + + log.Println("Re-broadcasting own invitation") + InitiateClaimJoin(ClaimParties[0].ClaimBlockHeight) } if MyRole != "none" { @@ -112,8 +120,9 @@ func loadClaimJoinDB() { db.Load("ClaimJoin", "serializedPrivateKey", &serializedKey) myPrivateKey, _ = btcec.PrivKeyFromBytes(serializedKey) log.Println("Continue as ClaimJoin " + MyRole + " with pubKey " + MyPublicKey()) + } else if ClaimJoinHandler != "" { + log.Println("Have ClaimJoin invite from", ClaimJoinHandler) } - } // runs every block @@ -145,12 +154,12 @@ create_pset: return } + // sometimes the result omits inputs or outputs! if len(analyzed.Outputs) != len(ClaimParties)+2 || len(decoded.Inputs) != len(ClaimParties) { - ClaimStatus = "Misformed PSET, trying again" + log.Printf("Malformed PSET with %d inputs and %d outputs, trying again", len(decoded.Inputs), len(analyzed.Outputs)) claimPSET = "" db.Save("ClaimJoin", "claimPSET", &claimPSET) - db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) - log.Println(ClaimStatus) + errorCounter++ if errorCounter < 10 { goto create_pset @@ -361,12 +370,6 @@ func kickPeer(pubKey, reason string) { // (it means the message came back to you from a downstream peer) func Broadcast(fromNodeId string, message *Message) error { - client, cleanup, err := ps.GetClient(config.Config.RpcHost) - if err != nil { - return err - } - defer cleanup() - if myNodeId == "" { // populates myNodeId if getLndVersion() == 0 { @@ -374,9 +377,15 @@ func Broadcast(fromNodeId string, message *Message) error { } } - if fromNodeId == myNodeId || (fromNodeId != myNodeId && (message.Asset == "pegin_started" && keyToNodeId[message.Sender] == "" || message.Asset == "pegin_ended" && PeginHandler != "")) { + if fromNodeId == myNodeId || (fromNodeId != myNodeId && (message.Asset == "pegin_started" && keyToNodeId[message.Sender] == "" || message.Asset == "pegin_ended" && ClaimJoinHandler != "")) { // forward to everyone else + client, cleanup, err := ps.GetClient(config.Config.RpcHost) + if err != nil { + return err + } + defer cleanup() + res, err := ps.ListPeers(client) if err != nil { return err @@ -394,73 +403,83 @@ func Broadcast(fromNodeId string, message *Message) error { SendCustomMessage(cl, peer.NodeId, message) } } + } - if fromNodeId == myNodeId { - // do nothing more if this is my own broadcast - return nil - } + if fromNodeId == myNodeId { + // do nothing more if this is my own broadcast + return nil + } - // react to received broadcast - switch message.Asset { - case "pegin_started": - if MyRole == "initiator" { - // two simultaneous initiators conflict - if len(ClaimParties) < 2 { - MyRole = "none" - PeginHandler = "" - db.Save("ClaimJoin", "MyRole", MyRole) - db.Save("ClaimJoin", "PeginHandler", PeginHandler) - log.Println("Initiator collision, switching to 'none' and restarting with a random delay") - time.Sleep(time.Duration(mathRand.Intn(60)) * time.Second) - os.Exit(0) - } else { - log.Println("Initiator collision, staying as initiator because I have joiners") - break - } - } else if MyRole == "joiner" { - // already joined another group, ignore + // store for relaying further encrypted messages + if keyToNodeId[message.Sender] != fromNodeId { + keyToNodeId[message.Sender] = fromNodeId + db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) + } + + // react to received broadcast + switch message.Asset { + case "pegin_started": + if MyRole == "initiator" { + // two simultaneous initiators conflict, the earlier wins + if len(ClaimParties) > 1 || message.Amount > uint64(JoinBlockHeight) { + log.Println("Initiator collision, staying as initiator") + // Re-broadcast own invitation after a 60-second delay + time.AfterFunc(60*time.Second, func() { + log.Println("Re-broadcasting own invitation") + InitiateClaimJoin(ClaimParties[0].ClaimBlockHeight) + }) return nil + } else if message.Amount == uint64(JoinBlockHeight) { + // a tie, solved by random restart + EndClaimJoin("", "Initiator collision") + delay := mathRand.Intn(4)*10 + 10 + log.Printf("Initiator collision, switching to 'none' and restarting with %d seconds delay", delay) + restarting = true + time.Sleep(time.Duration(delay) * time.Second) + os.Exit(0) + } else { + MyRole = "none" + db.Save("ClaimJoin", "MyRole", MyRole) } + } else if MyRole == "joiner" { + // already joined another group, ignore + return nil + } + if ClaimJoinHandler != message.Sender { // where to forward claimjoin request - PeginHandler = message.Sender + ClaimJoinHandler = message.Sender // Time limit to apply is communicated via Amount - ClaimBlockHeight = uint32(message.Amount) + JoinBlockHeight = uint32(message.Amount) ClaimStatus = "Received invitation to ClaimJoin" // reset counter of join attempts joinCounter = 0 - log.Println(ClaimStatus) + log.Println(ClaimStatus, "from", ClaimJoinHandler) // persist to db - db.Save("ClaimJoin", "PeginHandler", PeginHandler) - db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + db.Save("ClaimJoin", "ClaimJoinHandler", ClaimJoinHandler) + db.Save("ClaimJoin", "JoinBlockHeight", JoinBlockHeight) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + } - case "pegin_ended": - // only trust the message from the original handler - if MyRole == "joiner" && PeginHandler == message.Sender && config.Config.PeginClaimScript != "done" { - txId := string(message.Payload) - if txId != "" { - ClaimStatus = "ClaimJoin pegin successful! Liquid TxId: " + txId - // signal to telegram bot - config.Config.PeginTxId = txId - config.Config.PeginClaimScript = "done" - } else { - ClaimStatus = "Invitation to ClaimJoin revoked" - } - log.Println(ClaimStatus) - resetClaimJoin() + case "pegin_ended": + // only trust the message from the original handler + if MyRole == "joiner" && ClaimJoinHandler == message.Sender && config.Config.PeginClaimScript != "done" { + txId := string(message.Payload) + if txId != "" { + ClaimStatus = "ClaimJoin pegin successful! Liquid TxId: " + txId + // signal to telegram bot + config.Config.PeginTxId = txId + config.Config.PeginClaimScript = "done" + } else { + ClaimStatus = "Invitation to ClaimJoin revoked" } + log.Println(ClaimStatus) + resetClaimJoin() } } - if keyToNodeId[message.Sender] != fromNodeId { - // store for relaying further encrypted messages - keyToNodeId[message.Sender] = fromNodeId - db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) - } - return nil } @@ -508,17 +527,6 @@ func SendCoordination(destinationPubKey string, message *Coordination) bool { // Either forward to final destination or decrypt and process func Process(message *Message, senderNodeId string) { - if !config.Config.PeginClaimJoin { - // spam - return - } - - cl, clean, er := GetClient() - if er != nil { - return - } - defer clean() - if keyToNodeId[message.Sender] != senderNodeId { // save source key map keyToNodeId[message.Sender] = senderNodeId @@ -526,7 +534,7 @@ func Process(message *Message, senderNodeId string) { db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) } - if message.Destination == MyPublicKey() { + if message.Destination == MyPublicKey() && config.Config.PeginClaimJoin { // Decrypt the message using my private key plaintext, err := eciesDecrypt(myPrivateKey, message.Payload) if err != nil { @@ -558,13 +566,14 @@ func Process(message *Message, senderNodeId string) { } if ok, newClaimBlockHeight, status := addClaimParty(&msg.Joiner); ok { - ClaimBlockHeight = max(ClaimBlockHeight, newClaimBlockHeight) - if SendCoordination(msg.Joiner.PubKey, &Coordination{ Action: "confirm_add", - ClaimBlockHeight: ClaimBlockHeight, + ClaimBlockHeight: max(ClaimBlockHeight, newClaimBlockHeight), Status: status, }) { + ClaimBlockHeight = max(ClaimBlockHeight, newClaimBlockHeight) + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + ClaimStatus = "Added new joiner, total participants: " + strconv.Itoa(len(ClaimParties)) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) log.Println("Added "+msg.Joiner.PubKey+", total:", len(ClaimParties)) @@ -604,32 +613,37 @@ func Process(message *Message, senderNodeId string) { claimPSET = "" // persist to db db.Save("ClaimJoin", "claimPSET", claimPSET) + if len(ClaimParties) > 2 { + // inform the other joiners of the new ClaimBlockHeight + for i := 1; i <= len(ClaimParties)-2; i++ { + SendCoordination(ClaimParties[i].PubKey, &Coordination{ + Action: "confirm_add", + ClaimBlockHeight: ClaimBlockHeight, + Status: "One peer left, total participants: " + strconv.Itoa(len(ClaimParties)), + }) + } + } } else { log.Println("Cannot remove joiner, not in the list") } case "confirm_add": ClaimBlockHeight = msg.ClaimBlockHeight + ClaimJoinHandler = message.Sender MyRole = "joiner" ClaimStatus = msg.Status log.Println(ClaimStatus) - // persist to db db.Save("ClaimJoin", "MyRole", MyRole) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + db.Save("ClaimJoin", "ClaimJoinHandler", ClaimJoinHandler) case "refuse_add": log.Println(msg.Status) - // forget pegin handler, so that cannot initiate new ClaimJoin - PeginHandler = "" + // forget pegin handler, for not to try joining it again + forgetPubKey(ClaimJoinHandler) ClaimStatus = msg.Status - MyRole = "none" - - // persist to db - db.Save("ClaimJoin", "MyRole", MyRole) - db.Save("ClaimJoin", "PeginHandler", PeginHandler) - db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) case "process2": // process twice to blind and sign if MyRole != "joiner" { @@ -654,20 +668,20 @@ func Process(message *Message, senderNodeId string) { return } else { // remove yourself from ClaimJoin - if SendCoordination(PeginHandler, &Coordination{ + if SendCoordination(ClaimJoinHandler, &Coordination{ Action: "remove", Joiner: *ClaimParties[0], }) { - ClaimBlockHeight = 0 // forget pegin handler, so that cannot initiate new ClaimJoin - PeginHandler = "" + JoinBlockHeight = 0 + ClaimJoinHandler = "" ClaimStatus = "Left ClaimJoin group" MyRole = "none" log.Println(ClaimStatus) db.Save("ClaimJoin", "MyRole", &MyRole) db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) - db.Save("ClaimJoin", "PeginHandler", &PeginHandler) + db.Save("ClaimJoin", "ClaimJoinHandler", &ClaimJoinHandler) // disable ClaimJoin config.Config.PeginClaimJoin = false @@ -723,7 +737,7 @@ func Process(message *Message, senderNodeId string) { } // return PSET to Handler - if !SendCoordination(PeginHandler, &Coordination{ + if !SendCoordination(ClaimJoinHandler, &Coordination{ Action: "process", PSET: serializedPset, Status: ClaimStatus, @@ -736,10 +750,25 @@ func Process(message *Message, senderNodeId string) { } // message not to me, relay further + cl, clean, er := GetClient() + if er != nil { + return + } + defer clean() + destinationNodeId := keyToNodeId[message.Destination] if destinationNodeId == "" { log.Println("Cannot relay: destination PubKey " + message.Destination + " has no matching NodeId") + // inform the sender + SendCustomMessage(cl, senderNodeId, &Message{ + Version: MessageVersion, + Memo: "unable", + Destination: message.Destination, + Sender: MyPublicKey(), + }) return + // forget the pubKey + forgetPubKey(message.Destination) } err := SendCustomMessage(cl, destinationNodeId, message) @@ -748,6 +777,24 @@ func Process(message *Message, senderNodeId string) { } } +// no message route to destination +func forgetPubKey(destination string) { + // destination pubkey was invalid + if ClaimJoinHandler == destination { + if MyRole == "joiner" { + MyRole = "none" + ClaimStatus = "Unable to contact Initiator, resetting" + log.Println(ClaimStatus) + db.Save("ClaimJoin", "MyRole", MyRole) + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + } + ClaimJoinHandler = "" + db.Save("ClaimJoin", "ClaimJoinHandler", ClaimJoinHandler) + } + keyToNodeId[destination] = "" + db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) +} + // called for claim join initiator after his pegin funding tx confirms func InitiateClaimJoin(claimBlockHeight uint32) bool { if myNodeId == "" { @@ -757,11 +804,34 @@ func InitiateClaimJoin(claimBlockHeight uint32) bool { } } - myPrivateKey = generatePrivateKey() - if myPrivateKey != nil { - // persist to db - savePrivateKey() - } else { + if myPrivateKey == nil { + myPrivateKey = generatePrivateKey() + if myPrivateKey != nil { + // persist to db + savePrivateKey() + } else { + return false + } + } + + if len(ClaimParties) != 1 || ClaimParties[0].PubKey != MyPublicKey() { + party := createClaimParty(claimBlockHeight) + if party != nil { + // initiate array of claim parties + ClaimParties = nil + ClaimParties = append(ClaimParties, party) + ClaimBlockHeight = claimBlockHeight + JoinBlockHeight = claimBlockHeight - 1 + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + db.Save("ClaimJoin", "JoinBlockHeight", JoinBlockHeight) + db.Save("ClaimJoin", "claimParties", ClaimParties) + } else { + return false + } + } + + if restarting { + // resolving initiator collision return false } @@ -769,22 +839,15 @@ func InitiateClaimJoin(claimBlockHeight uint32) bool { Version: MessageVersion, Memo: "broadcast", Asset: "pegin_started", - Amount: uint64(claimBlockHeight), + Amount: uint64(JoinBlockHeight), Sender: MyPublicKey(), }) == nil { - ClaimBlockHeight = claimBlockHeight - party := createClaimParty(claimBlockHeight) - if party != nil { - // initiate array of claim parties - ClaimParties = nil - ClaimParties = append(ClaimParties, party) + if len(ClaimParties) == 1 { ClaimStatus = "Invites sent, awaiting joiners" // persist to db - db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - db.Save("ClaimJoin", "claimParties", ClaimParties) - return true } + return true } return false } @@ -802,7 +865,7 @@ func EndClaimJoin(txId string, status string) bool { Version: MessageVersion, Memo: "broadcast", Asset: "pegin_ended", - Amount: uint64(0), + Amount: uint64(ClaimBlockHeight), Sender: MyPublicKey(), Payload: []byte(txId), Destination: status, @@ -822,16 +885,18 @@ func EndClaimJoin(txId string, status string) bool { func resetClaimJoin() { // eraze all traces ClaimBlockHeight = 0 + JoinBlockHeight = 0 ClaimParties = nil MyRole = "none" - PeginHandler = "" - ClaimStatus = "No ClaimJoin peg-in is pending" + ClaimJoinHandler = "" + ClaimStatus = "No ClaimJoin pegin is pending" keyToNodeId = make(map[string]string) // persist to db db.Save("ClaimJoin", "claimParties", ClaimParties) - db.Save("ClaimJoin", "PeginHandler", PeginHandler) + db.Save("ClaimJoin", "ClaimJoinHandler", ClaimJoinHandler) db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + db.Save("ClaimJoin", "JoinBlockHeight", JoinBlockHeight) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) db.Save("ClaimJoin", "MyRole", MyRole) db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) @@ -839,13 +904,45 @@ func resetClaimJoin() { // called for ClaimJoin joiner candidate after his pegin funding tx confirms func JoinClaimJoin(claimBlockHeight uint32) bool { - if joinCounter > 3 { + if ClaimJoinHandler == "" { + return false + } + + if joinCounter > 2 { + // no reply, remove yourself from ClaimJoin + SendCoordination(ClaimJoinHandler, &Coordination{ + Action: "remove", + Joiner: *ClaimParties[0], + }) + forgetPubKey(ClaimJoinHandler) ClaimStatus = "Initator does not respond, forget him" - log.Println(ClaimStatus) - PeginHandler = "" - db.Save("ClaimJoin", "PeginHandler", PeginHandler) - db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - return true + + // poll to find out a new ClaimJoinHandler + client, cleanup, err := ps.GetClient(config.Config.RpcHost) + if err != nil { + return false + } + defer cleanup() + + res, err := ps.ListPeers(client) + if err != nil { + return false + } + + cl, clean, er := GetClient() + if er != nil { + return false + } + defer clean() + + for _, peer := range res.GetPeers() { + SendCustomMessage(cl, peer.NodeId, &Message{ + Version: MessageVersion, + Memo: "poll", + }) + } + + return false } if myNodeId == "" { @@ -861,31 +958,34 @@ func JoinClaimJoin(claimBlockHeight uint32) bool { if myPrivateKey != nil { // persist to db savePrivateKey() + } else { return false } } - party := createClaimParty(claimBlockHeight) - if party != nil { - if SendCoordination(PeginHandler, &Coordination{ - Action: "add", - Joiner: *party, - ClaimBlockHeight: claimBlockHeight, - }) { - // increment counter - joinCounter++ - // initiate array of claim parties for single entry - ClaimParties = nil - ClaimParties = append(ClaimParties, party) - ClaimStatus = "Responded to invitation, awaiting confirmation" - // persist to db - db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - db.Save("ClaimJoin", "claimParties", ClaimParties) - ClaimBlockHeight = claimBlockHeight - return true - } + if len(ClaimParties) != 1 || ClaimParties[0].PubKey != MyPublicKey() { + // initiate array of claim parties for single entry + ClaimParties = nil + ClaimParties = append(ClaimParties, createClaimParty(claimBlockHeight)) + ClaimBlockHeight = claimBlockHeight + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + db.Save("ClaimJoin", "ClaimParties", ClaimParties) } + + if SendCoordination(ClaimJoinHandler, &Coordination{ + Action: "add", + Joiner: *ClaimParties[0], + ClaimBlockHeight: claimBlockHeight, + }) { + // increment counter + joinCounter++ + ClaimStatus = "Responded to invitation, awaiting confirmation" + // persist to db + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + return true + } + return false } @@ -935,21 +1035,32 @@ func createClaimParty(claimBlockHeight uint32) *ClaimParty { // add claim party to the list func addClaimParty(newParty *ClaimParty) (bool, uint32, string) { + txHeight := uint32(internet.GetTxHeight(newParty.TxId)) + claimHight := txHeight + PeginBlocks - 1 + + // verify claimBlockHeight + if txHeight > 0 && claimHight != newParty.ClaimBlockHeight { + log.Printf("New joiner's ClaimBlockHeight was wrong: %d vs %d correct", newParty.ClaimBlockHeight, claimHight) + } else { + // cannot verify, have to trust the joiner + claimHight = newParty.ClaimBlockHeight + } + for _, party := range ClaimParties { if party.ClaimScript == newParty.ClaimScript { // is already in the list - return true, 0, "Was already in the list, total participants: " + strconv.Itoa(len(ClaimParties)) + return true, claimHight, "Successfully joined, total participants: " + strconv.Itoa(len(ClaimParties)) } } if len(ClaimParties) == maxParties { - return false, 0, "Refused to join, over limit of " + strconv.Itoa(maxParties) + return false, claimHight, "Refuse to add, over limit of " + strconv.Itoa(maxParties) } - // verify claimBlockHeight + // verify TxOutProof proof, err := bitcoin.GetTxOutProof(newParty.TxId) if err != nil { - return false, 0, "Refused to join, TX not confirmed" + return false, claimHight, "Refuse to add, TX not confirmed" } if proof != newParty.TxoutProof { @@ -957,16 +1068,6 @@ func addClaimParty(newParty *ClaimParty) (bool, uint32, string) { newParty.TxoutProof = proof } - txHeight := uint32(internet.GetTxHeight(newParty.TxId)) - claimHight := txHeight + PeginBlocks - 1 - - if txHeight > 0 && claimHight != newParty.ClaimBlockHeight { - log.Printf("New joiner's ClaimBlockHeight was wrong: %d vs %d correct", newParty.ClaimBlockHeight, claimHight) - } else { - // cannot verify, have to trust the joiner - claimHight = newParty.ClaimBlockHeight - } - ClaimParties = append(ClaimParties, newParty) // persist to db @@ -1197,6 +1298,7 @@ func verifyPSET() bool { } } + log.Println(ClaimParties[0].Address, addressInfo.Unconfidential, ClaimParties[0].Amount) log.Println(claimPSET) return false diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index 2fc10b9..2a5e245 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -338,7 +338,7 @@ func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate uint minConf := uint16(1) multiplier := uint64(1000) if !subtractFeeFromAmount && config.Config.Chain == "mainnet" { - multiplier = 935 // better sets fee rate for peg-in tx with change + multiplier = 935 // better sets fee rate for pegin tx with change } res, err := client.WithdrawWithUtxos( @@ -1320,10 +1320,11 @@ func MyPublicKey() string { return "" } func EndClaimJoin(a, b string) {} var ( - PeginHandler = "" + ClaimJoinHandler = "" MyRole = "none" ClaimStatus = "" - ClaimBlockHeight = uint32(0) + ClaimBlockHeight uint32 + JoinBlockHeight uint32 ClaimParties []int ) diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index b0e29eb..170655d 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -269,7 +269,7 @@ func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate uint var psbtBytes []byte if subtractFeeFromAmount && CanRBF() { - // new template since for LND 0.18+ + // new template since LND 0.18+ // change lockID to custom and construct manual psbt lockId = myLockId psbtBytes, err = fundPsbtSpendAll(cl, utxos, addr, feeRate) @@ -346,6 +346,7 @@ func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate uint _, err = cl.PublishTransaction(ctx, req) if err != nil { log.Println("PublishTransaction:", err) + log.Println("RawHex:", hex.EncodeToString(rawTx)) releaseOutputs(cl, utxos, &lockId) return nil, err } @@ -603,7 +604,7 @@ func BumpPeginFee(feeRate uint64, label string) (*SentResult, error) { func doCPFP(cl walletrpc.WalletKitClient, outputs []*lnrpc.OutputDetail, newFeeRate uint64) error { if len(outputs) == 1 { - return errors.New("peg-in transaction has no change output, not possible to CPFP") + return errors.New("pegin transaction has no change output, not possible to CPFP") } // find change output @@ -1219,29 +1220,31 @@ func subscribeMessages(ctx context.Context, client lnrpc.LightningClient) error nodeId := hex.EncodeToString(data.Peer) - // received broadcast of pegin status - // msg.Asset: "pegin_started" or "pegin_ended" - if msg.Memo == "broadcast" { + switch msg.Memo { + case "broadcast": + // received broadcast of pegin status + // msg.Asset: "pegin_started" or "pegin_ended" err = Broadcast(nodeId, &msg) if err != nil { log.Println(err) } - } - // messages related to pegin claim join - if msg.Memo == "process" && config.Config.PeginClaimJoin { + case "unable": + forgetPubKey(msg.Destination) + + case "process": + // messages related to pegin claimjoin Process(&msg, nodeId) - } - // received request for information - if msg.Memo == "poll" { - if MyRole == "initiator" && MyPublicKey() != "" && len(ClaimParties) < maxParties && GetBlockHeight(client) < ClaimBlockHeight { + case "poll": + // received request for information + if MyRole == "initiator" && GetBlockHeight(client) < JoinBlockHeight { // repeat pegin start info SendCustomMessage(client, nodeId, &Message{ Version: MessageVersion, Memo: "broadcast", Asset: "pegin_started", - Amount: uint64(ClaimBlockHeight), + Amount: uint64(JoinBlockHeight), Sender: MyPublicKey(), }) } @@ -1277,10 +1280,9 @@ func subscribeMessages(ctx context.Context, client lnrpc.LightningClient) error } } } - } - // received information - if msg.Memo == "balance" { + case "balance": + // received information ts := time.Now().Unix() if msg.Asset == "lbtc" { if LiquidBalances[nodeId] == nil { diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index ff7ed8b..ac5a8bd 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -92,7 +92,7 @@ func main() { flag.Parse() if *showHelp { - fmt.Println("A lightweight server-side rendered Web UI for PeerSwap, which allows trustless p2p submarine swaps Lightning<->BTC and Lightning<->Liquid. Also facilitates BTC->Liquid peg-ins. PeerSwap with Liquid is a great cost efficient way to rebalance lightning channels.") + fmt.Println("A lightweight server-side rendered Web UI for PeerSwap, which allows trustless p2p submarine swaps Lightning<->BTC and Lightning<->Liquid. Also facilitates BTC->Liquid pegins. PeerSwap with Liquid is a great cost efficient way to rebalance lightning channels.") fmt.Println("Usage:") flag.PrintDefaults() os.Exit(0) @@ -384,7 +384,7 @@ func onTimer(firstRun bool) { // skip first run so that forwards have time to download go ln.ApplyAutoFees() // Check if pegin can be claimed, initiated or joined - go checkPegin() + checkPegin() } // advertise Liquid balance @@ -1089,7 +1089,7 @@ func convertSwapsToHTMLTable(swaps []*peerswaprpc.PrettyPrintSwap, nodeId string return table } -// Check Peg-in status +// Check Pegin status func checkPegin() { cl, clean, er := ln.GetClient() if er != nil { @@ -1099,21 +1099,30 @@ func checkPegin() { currentBlockHeight := ln.GetBlockHeight(cl) - if currentBlockHeight > ln.ClaimBlockHeight && ln.MyRole == "none" && ln.PeginHandler != "" { + if currentBlockHeight > ln.JoinBlockHeight && ln.MyRole == "none" && ln.ClaimJoinHandler != "" { // invitation expired - ln.PeginHandler = "" - db.Save("ClaimJoin", "PeginHandler", ln.PeginHandler) + ln.ClaimStatus = "No ClaimJoin pegin is pending" + log.Println("Invitation expired from", ln.ClaimJoinHandler) + telegramSendMessage("🧬 ClaimJoin Invitation expired") + + ln.ClaimJoinHandler = "" + db.Save("ClaimJoin", "ClaimStatus", ln.ClaimStatus) + db.Save("ClaimJoin", "ClaimJoinHandler", ln.ClaimJoinHandler) } if config.Config.PeginTxId == "" { // send telegram if received new ClaimJoin invitation - if peginInvite != ln.PeginHandler { - t := "🧬 Someone has started a new ClaimJoin pegin" - if ln.PeginHandler == "" { + if peginInvite != ln.ClaimJoinHandler { + t := "🧬 There is a ClaimJoin pegin pending" + if ln.ClaimJoinHandler == "" { t = "🧬 ClaimJoin pegin has ended" + } else { + duration := time.Duration(10*(ln.JoinBlockHeight-currentBlockHeight)) * time.Minute + formattedDuration := time.Time{}.Add(duration).Format("15h 04m") + t += ", time limit to join: " + formattedDuration } if telegramSendMessage(t) { - peginInvite = ln.PeginHandler + peginInvite = ln.ClaimJoinHandler } } return @@ -1145,7 +1154,7 @@ func checkPegin() { ln.MyRole = "none" config.Config.PeginClaimJoin = false config.Save() - ln.EndClaimJoin("", "Claimed individually") + ln.EndClaimJoin("", "Claim Block Height has passed") } return } @@ -1188,21 +1197,21 @@ func checkPegin() { } if failed { - log.Println("Peg-in claim FAILED!") + log.Println("Pegin claim FAILED!") log.Println("Mainchain TxId:", config.Config.PeginTxId) log.Println("Raw tx:", rawTx) log.Println("Proof:", proof) log.Println("Claim Script:", config.Config.PeginClaimScript) - telegramSendMessage("❗ Peg-in claim FAILED! See log for details.") + telegramSendMessage("❗ Pegin claim FAILED! See log for details.") } else { - log.Println("Peg-in successful! Liquid TxId:", txid) - telegramSendMessage("💸 Peg-in successfull! Liquid TxId: `" + txid + "`") + log.Println("Pegin successful! Liquid TxId:", txid) + telegramSendMessage("💸 Pegin successfull! Liquid TxId: `" + txid + "`") } } else { if config.Config.PeginClaimJoin { if ln.MyRole == "none" { claimHeight := currentBlockHeight + ln.PeginBlocks - uint32(confs) - if ln.PeginHandler == "" { + if ln.ClaimJoinHandler == "" { // I will coordinate this join if ln.InitiateClaimJoin(claimHeight) { t := "Sent ClaimJoin invitations" @@ -1215,16 +1224,14 @@ func checkPegin() { config.Config.PeginClaimJoin = false config.Save() } - } else if currentBlockHeight < ln.ClaimBlockHeight { + } else if currentBlockHeight <= ln.JoinBlockHeight { // join by replying to initiator if ln.JoinClaimJoin(claimHeight) { t := "Applied to ClaimJoin group" - log.Println(t + " " + ln.PeginHandler + " as " + ln.MyPublicKey()) + log.Println(t + " " + ln.ClaimJoinHandler + " as " + ln.MyPublicKey()) telegramSendMessage("🧬 " + t) } else { - log.Println("Failed to apply to ClaimJoin, continuing as a single pegin") - config.Config.PeginClaimJoin = false - config.Save() + log.Println("Failed to apply to ClaimJoin group", ln.ClaimJoinHandler) } } } diff --git a/cmd/psweb/telegram.go b/cmd/psweb/telegram.go index 00ebae6..8e8fd4d 100644 --- a/cmd/psweb/telegram.go +++ b/cmd/psweb/telegram.go @@ -89,7 +89,7 @@ func telegramStart() { case "/pegin": t := "" if config.Config.PeginTxId == "" { - t = "No pending peg-in or BTC withdrawal" + t = "No pending pegin or BTC withdrawal" } else { cl, clean, er := ln.GetClient() if er != nil { @@ -107,7 +107,7 @@ func telegramStart() { formattedDuration = "Past due!" } t = "🧬 " + ln.ClaimStatus - if ln.MyRole == "none" && ln.PeginHandler != "" { + if ln.MyRole == "none" && ln.ClaimJoinHandler != "" { t += ". Time left to apply: " + formattedDuration } else if confs > 0 { t += ". Claim ETA: " + formattedDuration @@ -174,7 +174,7 @@ func telegramConnect() { }, tgbotapi.BotCommand{ Command: "pegin", - Description: "Status of peg-in or BTC withdrawal", + Description: "Status of pegin or BTC withdrawal", }, tgbotapi.BotCommand{ Command: "autoswaps", diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index 8cf7c19..6cf492d 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -37,7 +37,7 @@
@@ -64,7 +64,7 @@
- +
{{if .CanClaimJoin}} @@ -77,9 +77,9 @@ @@ -110,23 +110,23 @@
- +
{{else}}
- {{if .IsPegin}} -
-
-

Peg-In Progress

-
-
-

-
+
+
+ {{if .IsPegin}} +

Pegin Progress

+ {{else}} +

Bitcoin Withdrawal {{if gt .Confirmations 0}}(Complete){{else}}(Pending){{end}}

+ {{end}}
- {{else}} -

Bitcoin Withdrawal {{if gt .Confirmations 0}}(Complete){{else}}(Pending){{end}}

- {{end}} +
+

+
+
- - - - + + + + @@ -652,15 +656,17 @@ text += "Total fee: " + formatWithThousandSeparators(fee) + " sats\n"; text += "Cost PPM: " + formatWithThousandSeparators(Math.round(fee * 1000000 / netAmount)); - let hours = "17 hours"; - {{if .CanClaimJoin}} - if (document.getElementById("claimJoin").checked) { - hours = "17-{{.ClaimJoinETA}} hours"; - } + {{if .IsPegin}} + let hours = "17 hours"; + {{if .CanClaimJoin}} + if (document.getElementById("claimJoin").checked) { + hours = "17-{{.ClaimJoinETA}} hours"; + } + {{end}} + + text += "\nClaim ETA: " + hours; {{end}} - text += "\nClaim ETA: " + hours; - if (subtractFee && {{.BitcoinBalance}} - peginAmount < 25000) { {{if .IsCLN}} text += "\nReserve for anchor fee bumping will be returned as change." From f95b20ba810860c2a3a974e9ba7e939fe34dfe0c Mon Sep 17 00:00:00 2001 From: Impa10r Date: Mon, 19 Aug 2024 19:18:10 +0200 Subject: [PATCH 30/98] Add reconnect --- .vscode/launch.json | 2 +- README.md | 2 +- cmd/psweb/liquid/elements.go | 31 +++ cmd/psweb/ln/claimjoin.go | 439 +++++++++++++++++++---------------- cmd/psweb/ln/cln.go | 2 +- cmd/psweb/ln/lnd.go | 52 +++++ cmd/psweb/main.go | 14 +- cmd/psweb/telegram.go | 2 +- 8 files changed, 333 insertions(+), 211 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index f7481a7..2c86c18 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,7 @@ "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - //"args": ["-datadir", "/home/vlad/.peerswap2_t4"] + "args": ["-datadir", "/home/vlad/.peerswap_t4"] //"args": ["-datadir", "/home/vlad/.peerswap2"] } ] diff --git a/README.md b/README.md index c6446fc..93dd365 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ sudo systemctl disable psweb Update: Since v1.2.0 this is handled via UI on the Bitcoin page. -To convert some BTC on your node into L-BTC you don't need any third party (but must run a full Bitcon node with txindex=1 enabled): +To convert some BTC on your node into L-BTC you don't need any third party (but must run a full Bitcon node with txindex=1, or manually provide block hash from mempool.space for ```getrawtransaction```): 1. Generate a special BTC address: ```elements-cli getpeginaddress```. Save claim_script for later. 2. Send BTC onchain: ```lncli sendcoins --amt -addr --sat_per_vbyte ``` diff --git a/cmd/psweb/liquid/elements.go b/cmd/psweb/liquid/elements.go index be106c8..f7cf041 100644 --- a/cmd/psweb/liquid/elements.go +++ b/cmd/psweb/liquid/elements.go @@ -726,6 +726,37 @@ func DecodeRawTransaction(hexTx string) (*Transaction, error) { return &response, nil } +func GetRawTransaction(txid string, result *Transaction) (string, error) { + client := ElementsClient() + service := &Elements{client} + + params := []interface{}{txid, result != nil} + + r, err := service.client.call("getrawtransaction", params, "") + if err = handleError(err, &r); err != nil { + return "", err + } + + raw := "" + if result == nil { + // return raw hex + err = json.Unmarshal([]byte(r.Result), &raw) + if err != nil { + log.Printf("GetRawTransaction unmarshall raw: %v", err) + return "", err + } + } else { + // decode into result + err = json.Unmarshal([]byte(r.Result), &result) + if err != nil { + log.Printf("GetRawTransaction decode: %v", err) + return "", err + } + } + + return raw, nil +} + func SendRawTransaction(hexTx string) (string, error) { client := ElementsClient() diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 857d326..edc7ab3 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -34,7 +34,7 @@ import ( // maximum number of participants in ClaimJoin const ( maxParties = 10 - PeginBlocks = 2 //102 + PeginBlocks = 10 //102 ) var ( @@ -108,6 +108,12 @@ func loadClaimJoinDB() { db.Load("ClaimJoin", "ClaimParties", &ClaimParties) if MyRole != "none" { + if config.Config.PeginTxId == "" { + // was claimed already + resetClaimJoin() + return + } + var serializedKey []byte db.Load("ClaimJoin", "serializedPrivateKey", &serializedKey) myPrivateKey, _ = btcec.PrivKeyFromBytes(serializedKey) @@ -115,8 +121,6 @@ func loadClaimJoinDB() { if MyRole == "initiator" { db.Load("ClaimJoin", "claimPSET", &claimPSET) - log.Println("Re-broadcasting own invitation") - InitiateClaimJoin(ClaimParties[0].ClaimBlockHeight) } } else if ClaimJoinHandler != "" { log.Println("Continue with ClaimJoin invite from", ClaimJoinHandler) @@ -124,7 +128,7 @@ func loadClaimJoinDB() { } // runs every block -func onBlock(blockHeight uint32) { +func OnBlock(blockHeight uint32) { if !config.Config.PeginClaimJoin || MyRole != "initiator" || len(ClaimParties) < 2 || blockHeight < ClaimBlockHeight { return } @@ -279,6 +283,12 @@ create_pset: return } + if decodedTx.DiscountVsize == 0 { + log.Println("Decoded transaction omits DiscountVsize, cancelling ClaimJoin") + EndClaimJoin("", "Final TX failure: no CT discount") + return + } + exactFee := (decodedTx.DiscountVsize / 10) + 1 if decodedTx.DiscountVsize%10 == 0 { exactFee = (decodedTx.DiscountVsize / 10) @@ -334,10 +344,14 @@ create_pset: func sendFailure(peer int, action string) { ClaimParties[peer].SentCount++ ClaimStatus = "Failed to send coordiantion" - log.Printf("Failure #%d to send '%s' to %s", ClaimParties[peer].SentCount, action, ClaimParties[peer].PubKey) + peerNodeId := keyToNodeId[ClaimParties[peer].PubKey] + log.Printf("Failure #%d to send '%s' to %s via %s", ClaimParties[peer].SentCount, action, ClaimParties[peer].PubKey, GetAlias(peerNodeId)) + if ClaimParties[peer].SentCount > 4 { // peer is not responding, kick him - kickPeer(ClaimParties[peer].PubKey, "being unresponsive") + if kickPeer(ClaimParties[peer].PubKey, "being unresponsive") { + sendToGroup("One peer was kicked, total participants: " + strconv.Itoa(len(ClaimParties))) + } if len(ClaimParties) < 2 { log.Println("Unable to send coordination, cancelling ClaimJoin") EndClaimJoin("", "Coordination failure") @@ -345,7 +359,7 @@ func sendFailure(peer int, action string) { } } -func kickPeer(pubKey, reason string) { +func kickPeer(pubKey, reason string) bool { if ok := removeClaimParty(pubKey); ok { ClaimStatus = "Peer " + pubKey + " kicked, total participants: " + strconv.Itoa(len(ClaimParties)) // persist to db @@ -360,9 +374,11 @@ func kickPeer(pubKey, reason string) { Action: "refuse_add", Status: "Kicked for " + reason, }) - } else { - EndClaimJoin("", "Coordination failure") + return true } + + EndClaimJoin("", "Coordination failure") + return false } // Called when received a broadcast custom message @@ -423,11 +439,6 @@ func Broadcast(fromNodeId string, message *Message) error { // two simultaneous initiators conflict, the earlier wins if len(ClaimParties) > 1 || ClaimJoinHandlerTS < message.TimeStamp { log.Println("Initiator collision, staying as initiator") - // Re-broadcast own invitation after a 60-second delay - /* time.AfterFunc(60*time.Second, func() { - log.Println("Re-broadcasting own invitation") - InitiateClaimJoin(ClaimParties[0].ClaimBlockHeight) - }) */ return nil } else { log.Println("Initiator collision, switching to 'none'") @@ -464,11 +475,35 @@ func Broadcast(fromNodeId string, message *Message) error { // only trust the message from the original handler if ClaimJoinHandler == message.Sender { txId := string(message.Payload) - if MyRole == "joiner" && txId != "" && config.Config.PeginClaimScript != "done" { - ClaimStatus = "ClaimJoin pegin successful! Liquid TxId: " + txId - // signal to telegram bot - config.Config.PeginTxId = txId - config.Config.PeginClaimScript = "done" + if MyRole == "joiner" && txId != "" && config.Config.PeginClaimScript != "done" && len(ClaimParties) == 1 { + // verify that my address was funded + var decoded liquid.Transaction + _, err := liquid.GetRawTransaction(txId, &decoded) + if err != nil { + ClaimStatus = "Error decoding posted transaction" + return err + } + + addressInfo, err := liquid.GetAddressInfo(ClaimParties[0].Address, config.Config.ElementsWallet) + if err != nil { + return nil + } + + ok := false + for _, output := range decoded.Vout { + if output.ScriptPubKey.Address == addressInfo.Unconfidential { + ok = true + break + } + } + if ok { + ClaimStatus = "ClaimJoin pegin successful! Liquid TxId: " + txId + // signal to telegram bot + config.Config.PeginTxId = txId + config.Config.PeginClaimScript = "done" + } else { + ClaimStatus = "My liquid address not found in the posted transaction" + } } else { ClaimStatus = "Invitation to ClaimJoin revoked" } @@ -548,220 +583,205 @@ func Process(message *Message, senderNodeId string) { db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) } - if message.Destination == MyPublicKey() && config.Config.PeginClaimJoin && len(ClaimParties) > 0 { - // Decrypt the message using my private key - plaintext, err := eciesDecrypt(myPrivateKey, message.Payload) - if err != nil { - log.Printf("Error decrypting payload: %s", err) - return - } - - // recover the struct - var msg Coordination - var buffer bytes.Buffer - - // Write the byte slice into the buffer - buffer.Write(plaintext) + if message.Destination == MyPublicKey() { + if config.Config.PeginClaimJoin && len(ClaimParties) > 0 { + // Decrypt the message using my private key + plaintext, err := eciesDecrypt(myPrivateKey, message.Payload) + if err != nil { + log.Printf("Error decrypting payload: %s", err) + return + } - // Deserialize binary data - decoder := gob.NewDecoder(&buffer) - if err := decoder.Decode(&msg); err != nil { - log.Printf("Received an incorrectly formed Coordination: %s", err) - return - } + // recover the struct + var msg Coordination + var buffer bytes.Buffer - newClaimPSET := base64.StdEncoding.EncodeToString(msg.PSET) + // Write the byte slice into the buffer + buffer.Write(plaintext) - switch msg.Action { - case "add": - if MyRole != "initiator" { - log.Printf("Cannot add a peer, not a claim initiator") + // Deserialize binary data + decoder := gob.NewDecoder(&buffer) + if err := decoder.Decode(&msg); err != nil { + log.Printf("Received an incorrectly formed Coordination: %s", err) return } - if ok, status := addClaimParty(&msg.Joiner); ok { - if SendCoordination(msg.Joiner.PubKey, &Coordination{ - Action: "confirm_add", - ClaimBlockHeight: max(ClaimBlockHeight, msg.ClaimBlockHeight), - Status: status, - }) { - ClaimBlockHeight = max(ClaimBlockHeight, msg.ClaimBlockHeight) - db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + newClaimPSET := base64.StdEncoding.EncodeToString(msg.PSET) - ClaimStatus = "Added new peer, total participants: " + strconv.Itoa(len(ClaimParties)) - db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - log.Println("Added "+msg.Joiner.PubKey+", total:", len(ClaimParties)) - - if len(ClaimParties) > 2 { - // inform the other joiners of the new ClaimBlockHeight - for i := 1; i <= len(ClaimParties)-2; i++ { - SendCoordination(ClaimParties[i].PubKey, &Coordination{ - Action: "confirm_add", - ClaimBlockHeight: ClaimBlockHeight, - Status: "Another peer joined, total participants: " + strconv.Itoa(len(ClaimParties)), - }) - } - } - } - } else { - if SendCoordination(msg.Joiner.PubKey, &Coordination{ - Action: "refuse_add", - Status: status, - }) { - log.Println("Refused new peer: ", status) + switch msg.Action { + case "add": + if MyRole != "initiator" { + log.Printf("Cannot add a peer, not a claim initiator") + return } - } - case "remove": - if MyRole != "initiator" { - log.Printf("Cannot remove a peer, not a claim initiator") - return - } + if ok, status := addClaimParty(&msg.Joiner); ok { + if SendCoordination(msg.Joiner.PubKey, &Coordination{ + Action: "confirm_add", + ClaimBlockHeight: max(ClaimBlockHeight, msg.ClaimBlockHeight), + Status: status, + }) { + ClaimBlockHeight = max(ClaimBlockHeight, msg.ClaimBlockHeight) + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - if removeClaimParty(msg.Joiner.PubKey) { - ClaimStatus = "Removed a peer, total participants: " + strconv.Itoa(len(ClaimParties)) - // persist to db - db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) - log.Println(ClaimStatus) - // erase PSET to start over - claimPSET = "" - // persist to db - db.Save("ClaimJoin", "claimPSET", claimPSET) - if len(ClaimParties) > 2 { - // inform the other joiners of the new ClaimBlockHeight - for i := 1; i <= len(ClaimParties)-2; i++ { - SendCoordination(ClaimParties[i].PubKey, &Coordination{ - Action: "confirm_add", - ClaimBlockHeight: ClaimBlockHeight, - Status: "One peer left, total participants: " + strconv.Itoa(len(ClaimParties)), - }) + ClaimStatus = "Added new peer, total participants: " + strconv.Itoa(len(ClaimParties)) + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + log.Println("Added "+msg.Joiner.PubKey+", total:", len(ClaimParties)) + sendToGroup("Another peer joined, total participants: " + strconv.Itoa(len(ClaimParties))) + } + } else { + if SendCoordination(msg.Joiner.PubKey, &Coordination{ + Action: "refuse_add", + Status: status, + }) { + log.Println("Refused new peer: ", status) } } - } else { - log.Println("Cannot remove peer, not in the list") - } - case "confirm_add": - ClaimBlockHeight = msg.ClaimBlockHeight - ClaimJoinHandler = message.Sender - MyRole = "joiner" - ClaimStatus = msg.Status - log.Println(ClaimStatus) - // persist to db - db.Save("ClaimJoin", "MyRole", MyRole) - db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) - db.Save("ClaimJoin", "ClaimJoinHandler", ClaimJoinHandler) + case "remove": + if MyRole != "initiator" { + log.Printf("Cannot remove a peer, not a claim initiator") + return + } - case "refuse_add": - log.Println(msg.Status) - // forget pegin handler, for not to try joining it again - forgetPubKey(ClaimJoinHandler) - ClaimStatus = msg.Status + if removeClaimParty(msg.Joiner.PubKey) { + ClaimStatus = "Removed a peer, total participants: " + strconv.Itoa(len(ClaimParties)) + // persist to db + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + log.Println(ClaimStatus) + // erase PSET to start over + claimPSET = "" + // persist to db + db.Save("ClaimJoin", "claimPSET", claimPSET) + sendToGroup("One peer left, total participants: " + strconv.Itoa(len(ClaimParties))) + } else { + log.Println("Cannot remove peer, not in the list") + } - case "process2": // process twice to blind and sign - if MyRole != "joiner" { - log.Println("received process2 while did not join") - return - } + case "confirm_add": + ClaimBlockHeight = msg.ClaimBlockHeight + ClaimJoinHandler = message.Sender + MyRole = "joiner" + ClaimStatus = msg.Status + log.Println(ClaimStatus) + // persist to db + db.Save("ClaimJoin", "MyRole", MyRole) + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + db.Save("ClaimJoin", "ClaimJoinHandler", ClaimJoinHandler) - // process my output - newClaimPSET, _, err = liquid.ProcessPSET(newClaimPSET, config.Config.ElementsWallet) - if err != nil { - log.Println("Unable to process PSET:", err) - return - } - fallthrough // continue to second pass + case "refuse_add": + log.Println(msg.Status) + // forget pegin handler, for not to try joining it again + forgetPubKey(ClaimJoinHandler) + ClaimStatus = msg.Status - case "process": // blind or sign - if !verifyPSET(newClaimPSET) { - log.Println("PSET verification failure!") - if MyRole == "initiator" { - // kick the joiner who returned broken PSET - kickPeer(message.Sender, "broken PSET return") + case "process2": // process twice to blind and sign + if MyRole != "joiner" { + log.Println("received process2 while did not join") return - } else { - // remove yourself from ClaimJoin - if SendCoordination(ClaimJoinHandler, &Coordination{ - Action: "remove", - Joiner: ClaimParties[0], - }) { - // forget pegin handler, so that cannot initiate new ClaimJoin - JoinBlockHeight = 0 - ClaimJoinHandler = "" - ClaimStatus = "Left ClaimJoin group" - MyRole = "none" - log.Println(ClaimStatus) - - db.Save("ClaimJoin", "MyRole", &MyRole) - db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) - db.Save("ClaimJoin", "ClaimJoinHandler", &ClaimJoinHandler) - } } - return - } - if MyRole == "initiator" { - // reset SentCount - for i, party := range ClaimParties { - if party.PubKey == message.Sender { - ClaimParties[i].SentCount = 0 - break + // process my output + newClaimPSET, _, err = liquid.ProcessPSET(newClaimPSET, config.Config.ElementsWallet) + if err != nil { + log.Println("Unable to process PSET:", err) + return + } + fallthrough // continue to second pass + + case "process": // blind or sign + if !verifyPSET(newClaimPSET) { + log.Println("PSET verification failure!") + if MyRole == "initiator" { + // kick the joiner who returned broken PSET + if kickPeer(message.Sender, "broken PSET return") { + sendToGroup("One peer was kicked, total participants: " + strconv.Itoa(len(ClaimParties))) + } + return + } else { + // remove yourself from ClaimJoin + if SendCoordination(ClaimJoinHandler, &Coordination{ + Action: "remove", + Joiner: ClaimParties[0], + }) { + // forget pegin handler, so that cannot initiate new ClaimJoin + JoinBlockHeight = 0 + ClaimJoinHandler = "" + ClaimStatus = "Left ClaimJoin group" + MyRole = "none" + log.Println(ClaimStatus) + + db.Save("ClaimJoin", "MyRole", &MyRole) + db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) + db.Save("ClaimJoin", "ClaimJoinHandler", &ClaimJoinHandler) + } } + return } - ClaimStatus = msg.Status - log.Println(ClaimStatus) + if MyRole == "initiator" { + // reset SentCount + for i, party := range ClaimParties { + if party.PubKey == message.Sender { + ClaimParties[i].SentCount = 0 + break + } + } - db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + ClaimStatus = msg.Status + log.Println(ClaimStatus) - // Save received claimPSET, execute onBlock to continue signing - db.Save("ClaimJoin", "claimPSET", &claimPSET) - onBlock(ClaimBlockHeight) - return - } + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - if MyRole != "joiner" { - log.Println("Received 'process' unexpected") - return - } + // Save received claimPSET, execute onBlock to continue signing + db.Save("ClaimJoin", "claimPSET", &claimPSET) + OnBlock(ClaimBlockHeight) + return + } - // process my output - claimPSET, _, err = liquid.ProcessPSET(claimPSET, config.Config.ElementsWallet) - if err != nil { - log.Println("Unable to process PSET:", err) - return - } + if MyRole != "joiner" { + log.Println("Received 'process' unexpected") + return + } - ClaimBlockHeight = msg.ClaimBlockHeight - ClaimStatus = msg.Status + " done" - log.Println(ClaimStatus) + // process my output + claimPSET, _, err = liquid.ProcessPSET(claimPSET, config.Config.ElementsWallet) + if err != nil { + log.Println("Unable to process PSET:", err) + return + } - db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + ClaimBlockHeight = msg.ClaimBlockHeight + ClaimStatus = msg.Status + " done" + log.Println(ClaimStatus) - serializedPset, err := base64.StdEncoding.DecodeString(claimPSET) - if err != nil { - log.Println("Unable to serialize PSET") - return - } + db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) + + serializedPset, err := base64.StdEncoding.DecodeString(claimPSET) + if err != nil { + log.Println("Unable to serialize PSET") + return + } - // return PSET to Handler - if !SendCoordination(ClaimJoinHandler, &Coordination{ - Action: "process", - PSET: serializedPset, - Status: ClaimStatus, - }) { - log.Println("Unable to send blind coordination, cancelling ClaimJoin") - EndClaimJoin("", "Coordination failure") + // return PSET to Handler + if !SendCoordination(ClaimJoinHandler, &Coordination{ + Action: "process", + PSET: serializedPset, + Status: ClaimStatus, + }) { + log.Println("Unable to send blind coordination, cancelling ClaimJoin") + EndClaimJoin("", "Coordination failure") + } } } return } // message not to me, relay further - cl, clean, er := GetClient() - if er != nil { + cl, clean, err := GetClient() + if err != nil { return } defer clean() @@ -769,25 +789,38 @@ func Process(message *Message, senderNodeId string) { destinationNodeId := keyToNodeId[message.Destination] if destinationNodeId == "" { log.Println("Cannot relay: destination PubKey " + message.Destination + " has no matching NodeId") - // inform the sender + // forget the pubKey + forgetPubKey(message.Destination) + // inform the sender that was unable to relay SendCustomMessage(cl, senderNodeId, &Message{ Version: MessageVersion, Memo: "unable", Destination: message.Destination, Sender: MyPublicKey(), }) - - // forget the pubKey - forgetPubKey(message.Destination) - return } log.Println("Relaying", message.Memo, "from", GetAlias(senderNodeId), "to", GetAlias(destinationNodeId)) - err := SendCustomMessage(cl, destinationNodeId, message) + err = SendCustomMessage(cl, destinationNodeId, message) if err != nil { log.Println("Cannot relay:", err) } + +} + +// pass information for all members of the group +func sendToGroup(status string) { + if len(ClaimParties) > 2 { + // inform the other joiners of the new ClaimBlockHeight + for i := 1; i <= len(ClaimParties)-2; i++ { + SendCoordination(ClaimParties[i].PubKey, &Coordination{ + Action: "confirm_add", + ClaimBlockHeight: ClaimBlockHeight, + Status: status, + }) + } + } } // no message route to destination @@ -1349,6 +1382,6 @@ func subscribeBlocks(conn *grpc.ClientConn) error { return err } - onBlock(blockEpoch.Height) + OnBlock(blockEpoch.Height) } } diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index 02d94c6..8c08863 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -1353,7 +1353,7 @@ func SendCustomMessage(client *glightning.Lightning, peerId string, message *Mes // ClaimJoin with CLN in not implemented, placeholder functions and variables: func loadClaimJoinDB() {} -func OnTimer() {} +func OnBlock() {} func InitiateClaimJoin(claimHeight uint32) bool { return false } func JoinClaimJoin(claimHeight uint32) bool { return false } func MyPublicKey() string { return "" } diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 8591f87..3a50493 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -1396,9 +1396,19 @@ func SendCustomMessage(client lnrpc.LightningClient, peerId string, message *Mes _, err = client.SendCustomMessage(context.Background(), req) if err != nil { + if err.Error() == "rpc error: code = NotFound desc = peer is not connected" { + // reconnect + reconnectPeer(peerId) + // try again + _, err = client.SendCustomMessage(context.Background(), req) + if err == nil { + goto success + } + } return err } +success: log.Printf("Sent %d bytes %s to %s", len(req.Data), message.Memo, GetAlias(peerId)) return nil @@ -2237,3 +2247,45 @@ func ForwardsLog(channelId uint64, fromTS int64) *[]DataPoint { return &log } + +func reconnectPeer(nodeId string) { + cl, clean, er := GetClient() + if er != nil { + return + } + defer clean() + + ctx := context.Background() + cl.DisconnectPeer(ctx, &lnrpc.DisconnectPeerRequest{PubKey: nodeId}) + info, err := cl.GetNodeInfo(ctx, &lnrpc.NodeInfoRequest{PubKey: nodeId, IncludeChannels: false}) + if err != nil { + log.Println("GetNodeInfo:", err) + return + } + skipTor := true + +try_to_connect: + for _, addr := range info.Node.Addresses { + if skipTor && strings.Contains(addr.Addr, ".onion") { + continue + } + _, err := cl.ConnectPeer(ctx, &lnrpc.ConnectPeerRequest{ + Addr: &lnrpc.LightningAddress{ + Pubkey: nodeId, + Host: addr.Addr, + }, + Perm: false, + Timeout: 10, + }) + if err == nil { + return + } + } + + if skipTor { + skipTor = false + goto try_to_connect + } else { + log.Println("Failed to reconnect to", GetAlias(nodeId)) + } +} diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index a30b393..2a0b9dc 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -479,6 +479,9 @@ func setLogging() (func(), error) { } } + // add new line after start up + log.Println("") + return cleanup, nil } @@ -1143,18 +1146,21 @@ func checkPegin() { // 10 blocks to wait before switching back to individual claim margin := uint32(10) if ln.MyRole == "initiator" && len(ln.ClaimParties) < 2 { - // if no one has joined, switch after 1 extra block - margin = 1 + // if no one has joined, switch on mturity + margin = 0 } if currentBlockHeight >= ln.ClaimBlockHeight+margin { // claim pegin individually - t := "ClaimJoin failed, falling back to the individual claim" + t := "ClaimJoin expired, falling back to the individual claim" log.Println(t) telegramSendMessage("🧬 " + t) ln.MyRole = "none" config.Config.PeginClaimJoin = false config.Save() - ln.EndClaimJoin("", "Claim Block Height has passed") + ln.EndClaimJoin("", "Reached Claim Block Height") + } else if currentBlockHeight >= ln.ClaimBlockHeight && ln.MyRole == "initiator" { + // proceed with + ln.OnBlock(currentBlockHeight) } return } diff --git a/cmd/psweb/telegram.go b/cmd/psweb/telegram.go index 8e8fd4d..c6b68fd 100644 --- a/cmd/psweb/telegram.go +++ b/cmd/psweb/telegram.go @@ -104,7 +104,7 @@ func telegramStart() { } formattedDuration := time.Time{}.Add(duration).Format("15h 04m") if duration < 0 { - formattedDuration = "Past due!" + formattedDuration = "Past due" } t = "🧬 " + ln.ClaimStatus if ln.MyRole == "none" && ln.ClaimJoinHandler != "" { From 623befe0d6af9c8641b40cc05d8b266aa36df6aa Mon Sep 17 00:00:00 2001 From: Impa10r Date: Mon, 19 Aug 2024 22:27:48 +0200 Subject: [PATCH 31/98] Fix invite looping --- cmd/psweb/ln/claimjoin.go | 99 ++++++++++++++++++++---------- cmd/psweb/ln/lnd.go | 16 +---- cmd/psweb/main.go | 4 -- cmd/psweb/templates/bitcoin.gohtml | 4 +- 4 files changed, 70 insertions(+), 53 deletions(-) diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index edc7ab3..bed79df 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -9,7 +9,6 @@ import ( "crypto/sha256" "encoding/base64" "encoding/gob" - "fmt" "io" "log" "strconv" @@ -28,6 +27,7 @@ import ( "peerswap-web/cmd/psweb/liquid" "peerswap-web/cmd/psweb/ps" + "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/chainrpc" ) @@ -384,46 +384,49 @@ func kickPeer(pubKey, reason string) bool { // Called when received a broadcast custom message // Forward the message to all direct peers, unless the source key is known already // (it means the message came back to you from a downstream peer) -func Broadcast(fromNodeId string, message *Message) error { +func Broadcast(fromNodeId string, message *Message) bool { if myNodeId == "" { // populates myNodeId if getLndVersion() == 0 { - return fmt.Errorf("Broadcast: Cannot get myNodeId") + return false } } + cl, clean, err := GetClient() + if err != nil { + return false + } + defer clean() + + sent := false + if fromNodeId == myNodeId || (fromNodeId != myNodeId && (message.Asset == "pegin_started" && keyToNodeId[message.Sender] == "" || message.Asset == "pegin_ended" && keyToNodeId[message.Sender] != "")) { // forward to everyone else - client, cleanup, err := ps.GetClient(config.Config.RpcHost) if err != nil { - return err + return false } defer cleanup() res, err := ps.ListPeers(client) if err != nil { - return err - } - - cl, clean, er := GetClient() - if er != nil { - return err + return false } - defer clean() for _, peer := range res.GetPeers() { // don't send it back to where it came from if peer.NodeId != fromNodeId { - SendCustomMessage(cl, peer.NodeId, message) + if SendCustomMessage(cl, peer.NodeId, message) == nil { + sent = true + } } } } if fromNodeId == myNodeId || message.Sender == MyPublicKey() { // do nothing more if this is my own broadcast - return nil + return sent } // store for relaying further encrypted messages @@ -439,18 +442,28 @@ func Broadcast(fromNodeId string, message *Message) error { // two simultaneous initiators conflict, the earlier wins if len(ClaimParties) > 1 || ClaimJoinHandlerTS < message.TimeStamp { log.Println("Initiator collision, staying as initiator") - return nil + // repeat pegin start info + SendCustomMessage(cl, fromNodeId, &Message{ + Version: MessageVersion, + Memo: "broadcast", + Asset: "pegin_started", + Amount: uint64(JoinBlockHeight), + Sender: MyPublicKey(), + }) + return false } else { log.Println("Initiator collision, switching to 'none'") MyRole = "none" + ClaimJoinHandler = "" db.Save("ClaimJoin", "MyRole", MyRole) + db.Save("ClaimJoin", "ClaimJoinHandler", ClaimJoinHandler) } } else if MyRole == "joiner" { // already joined another group, ignore - return nil + return false } - if ClaimJoinHandler != message.Sender { + if ClaimJoinHandler == "" { // where to forward claimjoin request ClaimJoinHandler = message.Sender // Save timestamp of the announcement @@ -481,12 +494,12 @@ func Broadcast(fromNodeId string, message *Message) error { _, err := liquid.GetRawTransaction(txId, &decoded) if err != nil { ClaimStatus = "Error decoding posted transaction" - return err + return false } addressInfo, err := liquid.GetAddressInfo(ClaimParties[0].Address, config.Config.ElementsWallet) if err != nil { - return nil + return false } ok := false @@ -516,7 +529,7 @@ func Broadcast(fromNodeId string, message *Message) error { } } - return nil + return false } // Encrypt and send message to an anonymous peer identified by base64 public key @@ -622,7 +635,7 @@ func Process(message *Message, senderNodeId string) { Status: status, }) { ClaimBlockHeight = max(ClaimBlockHeight, msg.ClaimBlockHeight) - db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) + db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) ClaimStatus = "Added new peer, total participants: " + strconv.Itoa(len(ClaimParties)) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) @@ -887,7 +900,8 @@ func InitiateClaimJoin(claimBlockHeight uint32) bool { Amount: uint64(JoinBlockHeight), Sender: MyPublicKey(), TimeStamp: ts, - }) == nil { + }) { + // at least one peer received it if len(ClaimParties) == 1 { // initial invite before everyone joined ClaimJoinHandlerTS = ts @@ -910,7 +924,7 @@ func EndClaimJoin(txId string, status string) bool { } } - if Broadcast(myNodeId, &Message{ + Broadcast(myNodeId, &Message{ Version: MessageVersion, Memo: "broadcast", Asset: "pegin_ended", @@ -918,17 +932,16 @@ func EndClaimJoin(txId string, status string) bool { Sender: MyPublicKey(), Payload: []byte(txId), Destination: status, - }) == nil { - if txId != "" { - log.Println("ClaimJoin pegin success! Liquid TxId:", txId) - // signal to telegram bot - config.Config.PeginTxId = txId - config.Config.PeginClaimScript = "done" - } - resetClaimJoin() - return true + }) + + if txId != "" { + log.Println("ClaimJoin pegin success! Liquid TxId:", txId) + // signal to telegram bot + config.Config.PeginTxId = txId + config.Config.PeginClaimScript = "done" } - return false + resetClaimJoin() + return true } func resetClaimJoin() { @@ -1038,6 +1051,26 @@ func JoinClaimJoin(claimBlockHeight uint32) bool { return false } +func shareInvite(client lnrpc.LightningClient, nodeId string) { + sender := ClaimJoinHandler + if MyRole == "initiator" { + sender = MyPublicKey() + } + if sender != "" && GetBlockHeight(client) < JoinBlockHeight { + // repeat pegin start info + if SendCustomMessage(client, nodeId, &Message{ + Version: MessageVersion, + Memo: "broadcast", + Asset: "pegin_started", + Amount: uint64(JoinBlockHeight), + Sender: sender, + TimeStamp: ClaimJoinHandlerTS, + }) == nil { + log.Printf("Shared intite %s from %s with %s", sender, GetAlias(keyToNodeId[sender]), GetAlias(nodeId)) + } + } +} + func createClaimParty(claimBlockHeight uint32) *ClaimParty { party := new(ClaimParty) party.TxId = config.Config.PeginTxId diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 3a50493..66883e8 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -1296,10 +1296,7 @@ func subscribeMessages(ctx context.Context, client lnrpc.LightningClient) error case "broadcast": // received broadcast of pegin status // msg.Asset: "pegin_started" or "pegin_ended" - err = Broadcast(nodeId, &msg) - if err != nil { - log.Println(err) - } + Broadcast(nodeId, &msg) case "unable": forgetPubKey(msg.Destination) @@ -1310,16 +1307,7 @@ func subscribeMessages(ctx context.Context, client lnrpc.LightningClient) error case "poll": // received request for information - if MyRole == "initiator" && GetBlockHeight(client) < JoinBlockHeight { - // repeat pegin start info - SendCustomMessage(client, nodeId, &Message{ - Version: MessageVersion, - Memo: "broadcast", - Asset: "pegin_started", - Amount: uint64(JoinBlockHeight), - Sender: MyPublicKey(), - }) - } + shareInvite(client, nodeId) if AdvertiseLiquidBalance { if SendCustomMessage(client, nodeId, &Message{ diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 2a0b9dc..120835e 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -1225,10 +1225,6 @@ func checkPegin() { telegramSendMessage("🧬 " + t) ln.MyRole = "initiator" db.Save("ClaimJoin", "MyRole", ln.MyRole) - } else { - log.Println("Failed to initiate ClaimJoin, continuing as a single pegin") - config.Config.PeginClaimJoin = false - config.Save() } } else if currentBlockHeight <= ln.JoinBlockHeight { // join by replying to initiator diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index 33f56cf..18e3458 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -77,9 +77,9 @@ From 0a2e74407c28a59071ee5fe8ed11dce8734e8229 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Mon, 19 Aug 2024 22:38:58 +0200 Subject: [PATCH 32/98] Fix ETA estimate --- cmd/psweb/templates/bitcoin.gohtml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index 18e3458..80b4fc1 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -656,16 +656,15 @@ text += "Total fee: " + formatWithThousandSeparators(fee) + " sats\n"; text += "Cost PPM: " + formatWithThousandSeparators(Math.round(fee * 1000000 / netAmount)); - {{if .IsPegin}} + if (isPegin) { let hours = "17 hours"; {{if .CanClaimJoin}} if (document.getElementById("claimJoin").checked) { hours = "17-{{.ClaimJoinETA}} hours"; } {{end}} - text += "\nClaim ETA: " + hours; - {{end}} + } if (subtractFee && {{.BitcoinBalance}} - peginAmount < 25000) { {{if .IsCLN}} From d401a843f89b121a0dbbb4d1554460f47eae83d5 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Tue, 20 Aug 2024 11:29:33 +0200 Subject: [PATCH 33/98] Fix reconnects --- cmd/psweb/ln/claimjoin.go | 123 ++++++++++++++++------------- cmd/psweb/ln/lnd.go | 57 ++++++++----- cmd/psweb/main.go | 7 +- cmd/psweb/templates/bitcoin.gohtml | 8 +- 4 files changed, 115 insertions(+), 80 deletions(-) diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index bed79df..6a0d523 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -34,7 +34,7 @@ import ( // maximum number of participants in ClaimJoin const ( maxParties = 10 - PeginBlocks = 10 //102 + PeginBlocks = 2 //102 ) var ( @@ -203,17 +203,16 @@ create_pset: return } - log.Println(ClaimStatus) - if !SendCoordination(ClaimParties[blinder].PubKey, &Coordination{ Action: action, PSET: serializedPset, Status: ClaimStatus, ClaimBlockHeight: ClaimBlockHeight, - }) { - sendFailure(blinder, action) + }, true) { + ClaimStatus = "Failed to send coordination" } + log.Println(ClaimStatus) db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) return } @@ -226,7 +225,6 @@ create_pset: signing++ if len(input.FinalScriptWitness) == 0 { ClaimStatus = "Signing " + strconv.Itoa(signing) + "/" + total - log.Println(ClaimStatus) if i == 0 { // my input, last to sign @@ -251,10 +249,11 @@ create_pset: PSET: serializedPset, Status: ClaimStatus, ClaimBlockHeight: ClaimBlockHeight, - }) { - sendFailure(i, "process") + }, true) { + ClaimStatus = "Failed to send coordination" } + log.Println(ClaimStatus) db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) return } @@ -341,30 +340,12 @@ create_pset: } } -func sendFailure(peer int, action string) { - ClaimParties[peer].SentCount++ - ClaimStatus = "Failed to send coordiantion" - peerNodeId := keyToNodeId[ClaimParties[peer].PubKey] - log.Printf("Failure #%d to send '%s' to %s via %s", ClaimParties[peer].SentCount, action, ClaimParties[peer].PubKey, GetAlias(peerNodeId)) - - if ClaimParties[peer].SentCount > 4 { - // peer is not responding, kick him - if kickPeer(ClaimParties[peer].PubKey, "being unresponsive") { - sendToGroup("One peer was kicked, total participants: " + strconv.Itoa(len(ClaimParties))) - } - if len(ClaimParties) < 2 { - log.Println("Unable to send coordination, cancelling ClaimJoin") - EndClaimJoin("", "Coordination failure") - } - } -} - -func kickPeer(pubKey, reason string) bool { +func kickPeer(pubKey, reason string) { if ok := removeClaimParty(pubKey); ok { - ClaimStatus = "Peer " + pubKey + " kicked, total participants: " + strconv.Itoa(len(ClaimParties)) + ClaimStatus = "Kicked out " + pubKey + ", total participants: " + strconv.Itoa(len(ClaimParties)) // persist to db db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) - log.Println(ClaimStatus) + log.Println(ClaimStatus, "for", reason) // erase PSET to start over claimPSET = "" // persist to db @@ -372,13 +353,14 @@ func kickPeer(pubKey, reason string) bool { // inform the offender SendCoordination(pubKey, &Coordination{ Action: "refuse_add", - Status: "Kicked for " + reason, - }) - return true + Status: "Kicked out for " + reason, + }, false) + // inform others + sendToGroup("One peer was kicked out, total participants: " + strconv.Itoa(len(ClaimParties))) + return } EndClaimJoin("", "Coordination failure") - return false } // Called when received a broadcast custom message @@ -535,7 +517,7 @@ func Broadcast(fromNodeId string, message *Message) bool { // Encrypt and send message to an anonymous peer identified by base64 public key // New keys are generated at the start of each ClaimJoin session // Peers track sources of encrypted messages to forward back the replies -func SendCoordination(destinationPubKey string, message *Coordination) bool { +func SendCoordination(destinationPubKey string, message *Coordination, needsResponse bool) bool { destinationNodeId := keyToNodeId[destinationPubKey] if destinationNodeId == "" { @@ -577,6 +559,21 @@ func SendCoordination(destinationPubKey string, message *Coordination) bool { log.Println("Cannot send custom message:", err) return false } + + if needsResponse { + // allow maximum 5 resends without a response + for i := 1; i < len(ClaimParties); i++ { + if ClaimParties[i].PubKey == destinationPubKey { + ClaimParties[i].SentCount++ + if ClaimParties[i].SentCount > 4 { + // peer is not responding, kick him + kickPeer(destinationPubKey, "being unresponsive") + return false + } + } + } + } + return true } @@ -597,6 +594,15 @@ func Process(message *Message, senderNodeId string) { } if message.Destination == MyPublicKey() { + // find the peer + for i := 1; i < len(ClaimParties); i++ { + if ClaimParties[i].PubKey == message.Sender { + // reset send conter + ClaimParties[i].SentCount = 0 + break + } + } + if config.Config.PeginClaimJoin && len(ClaimParties) > 0 { // Decrypt the message using my private key plaintext, err := eciesDecrypt(myPrivateKey, message.Payload) @@ -625,6 +631,10 @@ func Process(message *Message, senderNodeId string) { case "add": if MyRole != "initiator" { log.Printf("Cannot add a peer, not a claim initiator") + SendCoordination(msg.Joiner.PubKey, &Coordination{ + Action: "refuse_add", + Status: "Cannot add, no longer a claim initiator", + }, false) return } @@ -633,7 +643,7 @@ func Process(message *Message, senderNodeId string) { Action: "confirm_add", ClaimBlockHeight: max(ClaimBlockHeight, msg.ClaimBlockHeight), Status: status, - }) { + }, false) { ClaimBlockHeight = max(ClaimBlockHeight, msg.ClaimBlockHeight) db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) @@ -646,7 +656,7 @@ func Process(message *Message, senderNodeId string) { if SendCoordination(msg.Joiner.PubKey, &Coordination{ Action: "refuse_add", Status: status, - }) { + }, false) { log.Println("Refused new peer: ", status) } } @@ -654,6 +664,10 @@ func Process(message *Message, senderNodeId string) { case "remove": if MyRole != "initiator" { log.Printf("Cannot remove a peer, not a claim initiator") + SendCoordination(msg.Joiner.PubKey, &Coordination{ + Action: "refuse_add", + Status: "Cannot remove, not a claim initiator", + }, false) return } @@ -708,16 +722,14 @@ func Process(message *Message, senderNodeId string) { log.Println("PSET verification failure!") if MyRole == "initiator" { // kick the joiner who returned broken PSET - if kickPeer(message.Sender, "broken PSET return") { - sendToGroup("One peer was kicked, total participants: " + strconv.Itoa(len(ClaimParties))) - } + kickPeer(message.Sender, "invalid PSET return") return } else { // remove yourself from ClaimJoin if SendCoordination(ClaimJoinHandler, &Coordination{ Action: "remove", Joiner: ClaimParties[0], - }) { + }, false) { // forget pegin handler, so that cannot initiate new ClaimJoin JoinBlockHeight = 0 ClaimJoinHandler = "" @@ -734,12 +746,9 @@ func Process(message *Message, senderNodeId string) { } if MyRole == "initiator" { - // reset SentCount - for i, party := range ClaimParties { - if party.PubKey == message.Sender { - ClaimParties[i].SentCount = 0 - break - } + if msg.Status != ClaimStatus+" done" { + // not the expected reply, ignore + return } ClaimStatus = msg.Status @@ -747,8 +756,10 @@ func Process(message *Message, senderNodeId string) { db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - // Save received claimPSET, execute onBlock to continue signing + // Save received claimPSET db.Save("ClaimJoin", "claimPSET", &claimPSET) + + // execute onBlock to continue signing OnBlock(ClaimBlockHeight) return } @@ -783,8 +794,8 @@ func Process(message *Message, senderNodeId string) { Action: "process", PSET: serializedPset, Status: ClaimStatus, - }) { - log.Println("Unable to send blind coordination, cancelling ClaimJoin") + }, false) { + log.Println("Unable to send coordination, cancelling ClaimJoin") EndClaimJoin("", "Coordination failure") } } @@ -811,6 +822,7 @@ func Process(message *Message, senderNodeId string) { Destination: message.Destination, Sender: MyPublicKey(), }) + return } log.Println("Relaying", message.Memo, "from", GetAlias(senderNodeId), "to", GetAlias(destinationNodeId)) @@ -831,7 +843,7 @@ func sendToGroup(status string) { Action: "confirm_add", ClaimBlockHeight: ClaimBlockHeight, Status: status, - }) + }, false) } } } @@ -975,7 +987,7 @@ func JoinClaimJoin(claimBlockHeight uint32) bool { SendCoordination(ClaimJoinHandler, &Coordination{ Action: "remove", Joiner: ClaimParties[0], - }) + }, false) forgetPubKey(ClaimJoinHandler) ClaimStatus = "Initator does not respond, forget him" @@ -1039,7 +1051,7 @@ func JoinClaimJoin(claimBlockHeight uint32) bool { Action: "add", Joiner: ClaimParties[0], ClaimBlockHeight: claimBlockHeight, - }) { + }, false) { // increment counter joinCounter++ ClaimStatus = "Responded to invitation, awaiting confirmation" @@ -1359,7 +1371,12 @@ func verifyPSET(newClaimPSET string) bool { return false } - if MyRole == "Initiator" { + if MyRole == "initiator" { + if newClaimPSET == claimPSET { + log.Println("Peer returned identical PSET") + return false + } + decodedOld, err := liquid.DecodePSET(claimPSET) if err != nil { return false diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 66883e8..d76c3ee 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -67,6 +67,9 @@ var ( // inflight HTLCs mapped per Incoming channel inflightHTLCs = make(map[uint64][]*InflightHTLC) + // cache peer addresses for reconnects + peerAddresses = make(map[string][]*lnrpc.NodeAddress) + // las index for invoice subscriptions lastInvoiceSettleIndex uint64 @@ -1264,13 +1267,14 @@ func subscribeMessages(ctx context.Context, client lnrpc.LightningClient) error return err } - log.Println("Subscribed to messages") + log.Println("Subscribed to custom messages") for { data, err := stream.Recv() if err != nil { return err } + if data.Type == messageType { var msg Message var buffer bytes.Buffer @@ -1292,6 +1296,14 @@ func subscribeMessages(ctx context.Context, client lnrpc.LightningClient) error nodeId := hex.EncodeToString(data.Peer) + if peerAddresses[nodeId] == nil { + // cache peer addresses + info, err := client.GetNodeInfo(ctx, &lnrpc.NodeInfoRequest{PubKey: nodeId, IncludeChannels: false}) + if err == nil { + peerAddresses[nodeId] = info.Node.Addresses + } + } + switch msg.Memo { case "broadcast": // received broadcast of pegin status @@ -1386,11 +1398,12 @@ func SendCustomMessage(client lnrpc.LightningClient, peerId string, message *Mes if err != nil { if err.Error() == "rpc error: code = NotFound desc = peer is not connected" { // reconnect - reconnectPeer(peerId) - // try again - _, err = client.SendCustomMessage(context.Background(), req) - if err == nil { - goto success + if reconnectPeer(client, peerId) { + // try again + _, err = client.SendCustomMessage(context.Background(), req) + if err == nil { + goto success + } } } return err @@ -2236,28 +2249,28 @@ func ForwardsLog(channelId uint64, fromTS int64) *[]DataPoint { return &log } -func reconnectPeer(nodeId string) { - cl, clean, er := GetClient() - if er != nil { - return - } - defer clean() - +func reconnectPeer(client lnrpc.LightningClient, nodeId string) bool { ctx := context.Background() - cl.DisconnectPeer(ctx, &lnrpc.DisconnectPeerRequest{PubKey: nodeId}) - info, err := cl.GetNodeInfo(ctx, &lnrpc.NodeInfoRequest{PubKey: nodeId, IncludeChannels: false}) - if err != nil { - log.Println("GetNodeInfo:", err) - return + addresses := peerAddresses[nodeId] + + if addresses == nil { + info, err := client.GetNodeInfo(ctx, &lnrpc.NodeInfoRequest{PubKey: nodeId, IncludeChannels: false}) + if err == nil { + addresses = info.Node.Addresses + } else { + log.Printf("Cannot reconnect to %s: %s", GetAlias(nodeId), err) + return false + } } + skipTor := true try_to_connect: - for _, addr := range info.Node.Addresses { + for _, addr := range addresses { if skipTor && strings.Contains(addr.Addr, ".onion") { continue } - _, err := cl.ConnectPeer(ctx, &lnrpc.ConnectPeerRequest{ + _, err := client.ConnectPeer(ctx, &lnrpc.ConnectPeerRequest{ Addr: &lnrpc.LightningAddress{ Pubkey: nodeId, Host: addr.Addr, @@ -2266,7 +2279,7 @@ try_to_connect: Timeout: 10, }) if err == nil { - return + return true } } @@ -2276,4 +2289,6 @@ try_to_connect: } else { log.Println("Failed to reconnect to", GetAlias(nodeId)) } + + return false } diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 120835e..b490644 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -36,14 +36,15 @@ import ( const ( // App version tag version = "v1.7.0" - // Swap Out reserves are hardcoded here: // https://github.com/ElementsProject/peerswap/blob/c77a82913d7898d0d3b7c83e4a990abf54bd97e5/peerswaprpc/server.go#L105 swapOutChannelReserve = 5000 // https://github.com/ElementsProject/peerswap/blob/c77a82913d7898d0d3b7c83e4a990abf54bd97e5/swap/actions.go#L388 swapOutChainReserve = 20300 // Swap In reserves - swapFeeReserveLBTC = uint64(300) + swapFeeReserveLBTC = 300 + // Elements v23.2.2 introduced vsize discount + elementsdFeeDiscountedVersion = 230202 ) type SwapParams struct { @@ -130,7 +131,7 @@ func main() { defer cleanup() // identify if Elements Core supports CT discounts - hasDiscountedvSize = liquid.GetVersion() >= 230202 + hasDiscountedvSize = liquid.GetVersion() >= elementsdFeeDiscountedVersion // identify if liquid blockchain supports CT discounts liquidGenesisHash, err := liquid.GetBlockHash(0) if err == nil { diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index 80b4fc1..183b7a4 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -666,11 +666,13 @@ text += "\nClaim ETA: " + hours; } - if (subtractFee && {{.BitcoinBalance}} - peginAmount < 25000) { + if ({{.BitcoinBalance}} - peginAmount < 25000) { {{if .IsCLN}} - text += "\nReserve for anchor fee bumping will be returned as change." + if (subtractFee) { + text += "\nReserve for anchor fee bumping will be returned as change." + } {{else}} - text += "\nWARNING: No reserve is left for anchor fee bumping!" + text += "\nWARNING: Not enough reserve left for anchor fee bumping!" {{end}} } document.getElementById('result').innerText = text; From 5e55144c98383f6c3748f179845c7944e1fc43a6 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Tue, 20 Aug 2024 11:55:38 +0200 Subject: [PATCH 34/98] Add invite notification --- cmd/psweb/handlers.go | 2 ++ cmd/psweb/ln/claimjoin.go | 28 ++++++++++++++++++++-------- cmd/psweb/templates/homepage.gohtml | 10 +++++++--- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index 70e149d..b5f1732 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -180,6 +180,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request) { MempoolFeeRate float64 AutoSwapEnabled bool PeginPending bool + ClaimJoinInvite bool AdvertiseLiquid bool AdvertiseBitcoin bool } @@ -200,6 +201,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request) { Filter: nodeId != "" || state != "" || role != "", AutoSwapEnabled: config.Config.AutoSwapEnabled, PeginPending: config.Config.PeginTxId != "" && config.Config.PeginClaimScript != "", + ClaimJoinInvite: ln.ClaimJoinHandler != "", AdvertiseLiquid: ln.AdvertiseLiquidBalance, AdvertiseBitcoin: ln.AdvertiseBitcoinBalance, } diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 6a0d523..c1abcdf 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -34,7 +34,7 @@ import ( // maximum number of participants in ClaimJoin const ( maxParties = 10 - PeginBlocks = 2 //102 + PeginBlocks = 10 //102 ) var ( @@ -46,6 +46,8 @@ var ( ClaimJoinHandler string // timestamp of the pegin_started broadcast of the current ClaimJoinHandler ClaimJoinHandlerTS uint64 + // Pegin txid of the ClaimJoinHandler for rebroadcasts + ClaimJoinHandlerTxId string // when currently pending pegin can be claimed ClaimBlockHeight uint32 // time limit to join another claim @@ -426,11 +428,13 @@ func Broadcast(fromNodeId string, message *Message) bool { log.Println("Initiator collision, staying as initiator") // repeat pegin start info SendCustomMessage(cl, fromNodeId, &Message{ - Version: MessageVersion, - Memo: "broadcast", - Asset: "pegin_started", - Amount: uint64(JoinBlockHeight), - Sender: MyPublicKey(), + Version: MessageVersion, + Memo: "broadcast", + Asset: "pegin_started", + Amount: uint64(JoinBlockHeight), + Sender: MyPublicKey(), + TimeStamp: ClaimJoinHandlerTS, + Payload: []byte(config.Config.PeginTxId), }) return false } else { @@ -446,10 +450,16 @@ func Broadcast(fromNodeId string, message *Message) bool { } if ClaimJoinHandler == "" { - // where to forward claimjoin request + // verify that pegin has indeed started + _, err := bitcoin.GetTxOutProof(string(message.Payload)) + if err != nil { + log.Println("Failed to get Initiator's TxOutProof, ignoring invite") + return false + } + ClaimJoinHandler = message.Sender - // Save timestamp of the announcement ClaimJoinHandlerTS = message.TimeStamp + ClaimJoinHandlerTxId = string(message.Payload) // Time limit to apply is communicated via Amount JoinBlockHeight = uint32(message.Amount) // reset counter of join attempts @@ -912,6 +922,7 @@ func InitiateClaimJoin(claimBlockHeight uint32) bool { Amount: uint64(JoinBlockHeight), Sender: MyPublicKey(), TimeStamp: ts, + Payload: []byte(config.Config.PeginTxId), }) { // at least one peer received it if len(ClaimParties) == 1 { @@ -1077,6 +1088,7 @@ func shareInvite(client lnrpc.LightningClient, nodeId string) { Amount: uint64(JoinBlockHeight), Sender: sender, TimeStamp: ClaimJoinHandlerTS, + Payload: []byte(ClaimJoinHandlerTxId), }) == nil { log.Printf("Shared intite %s from %s with %s", sender, GetAlias(keyToNodeId[sender]), GetAlias(nodeId)) } diff --git a/cmd/psweb/templates/homepage.gohtml b/cmd/psweb/templates/homepage.gohtml index 2075788..30262af 100644 --- a/cmd/psweb/templates/homepage.gohtml +++ b/cmd/psweb/templates/homepage.gohtml @@ -26,10 +26,14 @@ {{if .PeginPending}} {{else}} - {{if and .AdvertiseLiquid .AllowSwapRequests}} - 📡 + {{if .ClaimJoinInvite}} + 🧬 {{else}} - ✔️{{else}}disabled">❌{{end}} + {{if and .AdvertiseLiquid .AllowSwapRequests}} + 📡 + {{else}} + ✔️{{else}}disabled">❌{{end}} + {{end}} {{end}} {{end}} {{if .AutoSwapEnabled}} From d601fb29d3b7f477b84a8448f4052060ca33705d Mon Sep 17 00:00:00 2001 From: Impa10r Date: Wed, 21 Aug 2024 13:38:32 +0200 Subject: [PATCH 35/98] Fix fee rounding --- cmd/psweb/ln/claimjoin.go | 92 ++++++++++++++++++++++++++++----------- cmd/psweb/ln/common.go | 3 +- cmd/psweb/utils.go | 2 +- 3 files changed, 69 insertions(+), 28 deletions(-) diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index c1abcdf..2c06588 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -11,7 +11,9 @@ import ( "encoding/gob" "io" "log" + "regexp" "strconv" + "strings" "time" mathRand "math/rand" @@ -95,6 +97,7 @@ type ClaimParty struct { FeeShare uint64 PubKey string SentCount uint + SentTime time.Time } // runs after restart, to continue if pegin is ongoing @@ -135,8 +138,10 @@ func OnBlock(blockHeight uint32) { return } + // initial fee estimate + totalFee := 41 + 30*(len(ClaimParties)-1) + errorCounter := 0 - totalFee := 36 * len(ClaimParties) create_pset: if claimPSET == "" { @@ -172,7 +177,6 @@ create_pset: } total := strconv.Itoa(len(ClaimParties)) - signing := 0 for i, output := range analyzed.Outputs { if output.Blind && output.Status == "unblinded" { @@ -196,7 +200,6 @@ create_pset: // the final blinder can blind and sign at once action = "process2" ClaimStatus += " & Signing 1/" + total - signing++ } serializedPset, err := base64.StdEncoding.DecodeString(claimPSET) @@ -224,12 +227,30 @@ create_pset: // Iterate through inputs in reverse order to sign for i := len(ClaimParties) - 1; i >= 0; i-- { input := decoded.Inputs[i] - signing++ + + signing := 1 + if strings.HasSuffix(ClaimStatus, "& Signing 1/"+total+" done") { + signing = 2 + } else if strings.HasPrefix(ClaimStatus, "Signing") && strings.HasSuffix(ClaimStatus, total+" done") { + // Define a regex pattern to find the first number + re := regexp.MustCompile(`\d+`) + match := re.FindString(ClaimStatus) + + // Convert the matched string to an integer + if match != "" { + num, err := strconv.Atoi(match) + if err == nil { + signing = num + 1 + } + } + } + if len(input.FinalScriptWitness) == 0 { ClaimStatus = "Signing " + strconv.Itoa(signing) + "/" + total if i == 0 { // my input, last to sign + log.Println(ClaimStatus) claimPSET, _, err = liquid.ProcessPSET(claimPSET, config.Config.ElementsWallet) if err != nil { log.Println("Unable to sign input, cancelling ClaimJoin:", err) @@ -290,9 +311,9 @@ create_pset: return } - exactFee := (decodedTx.DiscountVsize / 10) + 1 - if decodedTx.DiscountVsize%10 == 0 { - exactFee = (decodedTx.DiscountVsize / 10) + exactFee := (decodedTx.DiscountVsize / 10) + if decodedTx.DiscountVsize%10 != 0 { + exactFee++ } var feeValue int @@ -300,7 +321,7 @@ create_pset: // Iterate over the map for _, value := range decodedTx.Fee { - feeValue = int(value * 100_000_000) + feeValue = int(toSats(value)) found = true break } @@ -322,6 +343,7 @@ create_pset: db.Save("ClaimJoin", "claimPSET", &claimPSET) db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) + errorCounter = 0 goto create_pset } else { @@ -557,6 +579,19 @@ func SendCoordination(destinationPubKey string, message *Coordination, needsResp } defer clean() + partyN := 0 + for i := 1; i < len(ClaimParties); i++ { + if ClaimParties[i].PubKey == destinationPubKey { + partyN = i + break + } + } + + if needsResponse && time.Since(ClaimParties[partyN].SentTime) < 2*time.Second { + // resend too soon + return false + } + err = SendCustomMessage(cl, destinationNodeId, &Message{ Version: MessageVersion, Memo: "process", @@ -570,20 +605,17 @@ func SendCoordination(destinationPubKey string, message *Coordination, needsResp return false } - if needsResponse { + if needsResponse && partyN > 0 { // allow maximum 5 resends without a response - for i := 1; i < len(ClaimParties); i++ { - if ClaimParties[i].PubKey == destinationPubKey { - ClaimParties[i].SentCount++ - if ClaimParties[i].SentCount > 4 { - // peer is not responding, kick him - kickPeer(destinationPubKey, "being unresponsive") - return false - } - } + ClaimParties[partyN].SentCount++ + if ClaimParties[partyN].SentCount > 4 { + // peer is not responding, kick him + kickPeer(destinationPubKey, "being unresponsive") + return false } + // remember when last sent a message requiring response + ClaimParties[partyN].SentTime = time.Now() } - return true } @@ -635,8 +667,6 @@ func Process(message *Message, senderNodeId string) { return } - newClaimPSET := base64.StdEncoding.EncodeToString(msg.PSET) - switch msg.Action { case "add": if MyRole != "initiator" { @@ -715,20 +745,30 @@ func Process(message *Message, senderNodeId string) { case "process2": // process twice to blind and sign if MyRole != "joiner" { - log.Println("received process2 while did not join") + log.Println("Received process2 while not a joiner") return } // process my output + newClaimPSET := base64.StdEncoding.EncodeToString(msg.PSET) newClaimPSET, _, err = liquid.ProcessPSET(newClaimPSET, config.Config.ElementsWallet) if err != nil { - log.Println("Unable to process PSET:", err) + log.Println("Unable to encode PSET:", err) + return + } + + // save back into message + msg.PSET, err = base64.StdEncoding.DecodeString(newClaimPSET) + if err != nil { + log.Println("Unable to decode PSET:", err) return } + fallthrough // continue to second pass case "process": // blind or sign - if !verifyPSET(newClaimPSET) { + // if verified successfully, saves the new PSET as claimPSET + if !verifyPSET(base64.StdEncoding.EncodeToString(msg.PSET)) { log.Println("PSET verification failure!") if MyRole == "initiator" { // kick the joiner who returned broken PSET @@ -766,7 +806,7 @@ func Process(message *Message, senderNodeId string) { db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - // Save received claimPSET + // Save the received claimPSET db.Save("ClaimJoin", "claimPSET", &claimPSET) // execute onBlock to continue signing @@ -1436,7 +1476,7 @@ func subscribeBlocks(conn *grpc.ClientConn) error { return err } - log.Println("Subscribed to blocks") + log.Println("Subscribed to Bitcoin blocks") for { blockEpoch, err := stream.Recv() diff --git a/cmd/psweb/ln/common.go b/cmd/psweb/ln/common.go index f2b1dc7..1a8242e 100644 --- a/cmd/psweb/ln/common.go +++ b/cmd/psweb/ln/common.go @@ -1,6 +1,7 @@ package ln import ( + "math" "reflect" "strconv" "strings" @@ -220,7 +221,7 @@ var ( ) func toSats(amount float64) int64 { - return int64(float64(100000000) * amount) + return int64(math.Round(float64(100000000) * amount)) } // convert short channel id 2568777x70x1 to LND format diff --git a/cmd/psweb/utils.go b/cmd/psweb/utils.go index 3005a61..6991820 100644 --- a/cmd/psweb/utils.go +++ b/cmd/psweb/utils.go @@ -149,7 +149,7 @@ func simplifySwapState(state string) string { } func toSats(amount float64) uint64 { - return uint64(float64(100_000_000) * amount) + return uint64(math.Round(float64(100_000_000) * amount)) } func toUint(num int64) uint64 { From de6df3cc548ab1a1e81af67cc0e81fb59ed372b0 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Wed, 21 Aug 2024 14:22:22 +0200 Subject: [PATCH 36/98] CLN sendmessage switch to GOB --- .vscode/launch.json | 4 ++-- cmd/psweb/ln/cln.go | 31 +++++++++++++++++-------------- cmd/psweb/main.go | 5 ----- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2c86c18..41901b4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,11 +9,11 @@ "type": "go", "request": "launch", "mode": "auto", - "buildFlags": "-tags lnd", + "buildFlags": "-tags cln", "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - "args": ["-datadir", "/home/vlad/.peerswap_t4"] + //"args": ["-datadir", "/home/vlad/.peerswap_t4"] //"args": ["-datadir", "/home/vlad/.peerswap2"] } ] diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index 8c08863..4714425 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -3,9 +3,10 @@ package ln import ( + "bytes" "encoding/binary" + "encoding/gob" "encoding/hex" - "encoding/json" "errors" "fmt" "log" @@ -1328,36 +1329,38 @@ func ForwardsLog(channelId uint64, fromTS int64) *[]DataPoint { } func SendCustomMessage(client *glightning.Lightning, peerId string, message *Message) error { - // Marshal the Message struct to JSON - jsonData, err := json.Marshal(message) - if err != nil { + // Serialize the message using gob + var buffer bytes.Buffer + encoder := gob.NewEncoder(&buffer) + if err := encoder.Encode(message); err != nil { return err } // Create a buffer for the final output - data := make([]byte, 2+len(jsonData)) + data := make([]byte, 2+len(buffer.Bytes())) // Write the message type prefix binary.BigEndian.PutUint16(data[:2], uint16(messageType)) // Copy the JSON data to the buffer - copy(data[2:], jsonData) + copy(data[2:], buffer.Bytes()) - _, err = client.SendCustomMessage(peerId, hex.EncodeToString(data)) - if err != nil { + if _, err := client.SendCustomMessage(peerId, hex.EncodeToString(data)); err != nil { return err } + log.Printf("Sent %d bytes %s to %s", len(buffer.Bytes()), message.Memo, GetAlias(peerId)) + return nil } // ClaimJoin with CLN in not implemented, placeholder functions and variables: -func loadClaimJoinDB() {} -func OnBlock() {} -func InitiateClaimJoin(claimHeight uint32) bool { return false } -func JoinClaimJoin(claimHeight uint32) bool { return false } -func MyPublicKey() string { return "" } -func EndClaimJoin(a, b string) {} +func loadClaimJoinDB() {} +func OnBlock(a uint32) {} +func InitiateClaimJoin(a uint32) bool { return false } +func JoinClaimJoin(a uint32) bool { return false } +func MyPublicKey() string { return "" } +func EndClaimJoin(a, b string) {} var ( ClaimJoinHandler = "" diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index b490644..fd2e871 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -132,11 +132,6 @@ func main() { // identify if Elements Core supports CT discounts hasDiscountedvSize = liquid.GetVersion() >= elementsdFeeDiscountedVersion - // identify if liquid blockchain supports CT discounts - liquidGenesisHash, err := liquid.GetBlockHash(0) - if err == nil { - hasDiscountedvSize = stringIsInSlice(liquidGenesisHash, []string{"6d955c95af04f1d14f5a6e6bd501508b37bddb6202aac6d99d0522a8cb7deef5"}) - } // Load persisted data from database ln.LoadDB() From e3f9b45cc08c9e2aa2845d5a3045d99cef03b78c Mon Sep 17 00:00:00 2001 From: Impa10r Date: Wed, 21 Aug 2024 16:03:49 +0200 Subject: [PATCH 37/98] Estimate discount vsize --- .vscode/launch.json | 2 +- CHANGELOG.md | 5 +-- cmd/psweb/handlers.go | 29 ++++++++++++------ cmd/psweb/templates/bitcoin.gohtml | 2 +- cmd/psweb/templates/liquid.gohtml | 6 ++-- cmd/psweb/templates/peer.gohtml | 49 ++++++++++++++++++++++-------- 6 files changed, 65 insertions(+), 28 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 41901b4..eab2a29 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "type": "go", "request": "launch", "mode": "auto", - "buildFlags": "-tags cln", + "buildFlags": "-tags lnd", "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", diff --git a/CHANGELOG.md b/CHANGELOG.md index a945629..70ed402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ ## 1.7.0 -- LND: Implement confidential joint pegin claims with Elements v23.2.2+ -- Record exact fee rates on pegins and BTC withdrawals for RBF with 1 s/vb bump +- Correct swap fee estimates for discounted vsize (Elements v23.2.2+) +- LND: Implement confidential joint pegin claims (Elements v23.2.2+) +- Use exact fee rates for pegins and BTC withdrawals - Switch Custom Message serialization from JSON to GOB - Switch Custom Message type from 42067 to 42065 - Better estimate for swap-in fees and maximum swap amounts diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index b5f1732..83883ff 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -432,11 +432,15 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { if ptr := ln.LiquidBalances[peer.NodeId]; ptr != nil { peerLiquidBalance = int64(ptr.Amount) maxLiquidSwapOut = uint64(max(0, min(int64(maxLocalBalance)-swapOutChannelReserve, peerLiquidBalance-swapOutChainReserve))) - if maxLiquidSwapOut >= 100_000 { - selectedChannel = peer.Channels[maxLocalBalanceIndex].ChannelId - } else { - maxLiquidSwapOut = 0 - } + + } else { + maxLiquidSwapOut = uint64(max(0, int64(maxLocalBalance)-swapOutChannelReserve)) + } + + if maxLiquidSwapOut >= 100_000 { + selectedChannel = peer.Channels[maxLocalBalanceIndex].ChannelId + } else { + maxLiquidSwapOut = 0 } peerBitcoinBalance := int64(-1) @@ -444,11 +448,14 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { if ptr := ln.BitcoinBalances[peer.NodeId]; ptr != nil { peerBitcoinBalance = int64(ptr.Amount) maxBitcoinSwapOut = uint64(max(0, min(int64(maxLocalBalance)-swapOutChannelReserve, peerBitcoinBalance-swapOutChainReserve))) - if maxBitcoinSwapOut >= 100_000 { - selectedChannel = peer.Channels[maxLocalBalanceIndex].ChannelId - } else { - maxBitcoinSwapOut = 0 - } + } else { + maxBitcoinSwapOut = uint64(max(0, int64(maxLocalBalance)-swapOutChannelReserve)) + } + + if maxBitcoinSwapOut >= 100_000 { + selectedChannel = peer.Channels[maxLocalBalanceIndex].ChannelId + } else { + maxBitcoinSwapOut = 0 } // arbitrary haircuts to avoid 'no matching outgoing channel available' @@ -558,6 +565,7 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { MaxLiquidSwapIn int64 RecommendLiquidSwapIn int64 SelectedChannel uint64 + HasDiscountedvSize bool } data := Page{ @@ -606,6 +614,7 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { MaxLiquidSwapIn: maxLiquidSwapIn, RecommendLiquidSwapIn: recommendLiquidSwapIn, SelectedChannel: selectedChannel, + HasDiscountedvSize: hasDiscountedvSize, } // executing template named "peer" diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index 183b7a4..76d3b12 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -51,7 +51,7 @@ -
+
diff --git a/cmd/psweb/templates/liquid.gohtml b/cmd/psweb/templates/liquid.gohtml index 56f73ab..9fcf1cc 100644 --- a/cmd/psweb/templates/liquid.gohtml +++ b/cmd/psweb/templates/liquid.gohtml @@ -123,7 +123,7 @@
-
+
@@ -185,7 +185,9 @@
@@ -291,7 +291,7 @@ document.getElementById('sendTab').classList.remove("is-active"); document.getElementById("sendAddressField").style.display = "none"; document.getElementById('sendAddress').removeAttribute('required'); - document.getElementById("sendButton").value = "Start Peg-In"; + document.getElementById("sendButton").value = "Start Pegin"; document.getElementById("isPegin").value = "true"; {{if .CanClaimJoin}} document.getElementById("claimJoinField").style.display = ""; @@ -461,7 +461,7 @@ let outputsP2WPKH = change; // Number of P2WPKH outputs {{end}} - // there is always one P2SH output for peg-in address + // there is always one P2SH output for pegin address let outputsP2SH = 1; // Initialize total UTXO amount and UTXO counters @@ -642,7 +642,7 @@ let text = "Transaction size: " + vbyteSize + " vBytes\n"; if (isPegin) { - //liquid peg-in fee estimate + //liquid pegin fee estimate let liquidFee = "45"; fee += 45; {{if .CanClaimJoin}} @@ -667,7 +667,7 @@ let hours = "17 hours"; {{if .CanClaimJoin}} if (document.getElementById("claimJoin").checked) { - hours = "17-34 hours"; + hours = "17-{{.ClaimJoinETA}} hours"; } {{end}} diff --git a/cmd/psweb/templates/homepage.gohtml b/cmd/psweb/templates/homepage.gohtml index e34124a..2075788 100644 --- a/cmd/psweb/templates/homepage.gohtml +++ b/cmd/psweb/templates/homepage.gohtml @@ -24,7 +24,7 @@

🌊 {{fmt .LiquidBalance}} {{if .PeginPending}} - + {{else}} {{if and .AdvertiseLiquid .AllowSwapRequests}} 📡 From 30cda29fd5b99385d8f86552d4d441d080fd02ce Mon Sep 17 00:00:00 2001 From: Impa10r Date: Wed, 14 Aug 2024 14:46:11 +0200 Subject: [PATCH 20/98] Joiner claimparty save bug --- CHANGELOG.md | 1 + cmd/psweb/config/lnd.go | 2 +- cmd/psweb/ln/claimjoin.go | 138 ++++++++++++++++++++------------------ cmd/psweb/ln/common.go | 9 +-- 4 files changed, 79 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb1c897..5eb2917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 1.7.0 +- Implemented confidential joint pegin claims (ClaimJoin) - Switched Custom Message serialization from JSON to GOB - Switched Custom Message type from 42067 to 42065 - Better estimate swap-in fee and maximum swap amount diff --git a/cmd/psweb/config/lnd.go b/cmd/psweb/config/lnd.go index 887c8b0..ee48944 100644 --- a/cmd/psweb/config/lnd.go +++ b/cmd/psweb/config/lnd.go @@ -45,7 +45,7 @@ func LoadPS() { Config.ElementsPort = port } - // on first start without config there will be no elements user and password + // on the first start without config there will be no elements user and password if Config.ElementsPass == "" || Config.ElementsUser == "" { // check in peerswap.conf Config.ElementsPass = GetPeerswapLNDSetting("elementsd.rpcpass") diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 0657e73..727e50a 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -12,7 +12,6 @@ import ( "fmt" "io" "log" - "os" "strconv" "time" @@ -26,7 +25,6 @@ import ( "peerswap-web/cmd/psweb/bitcoin" "peerswap-web/cmd/psweb/config" "peerswap-web/cmd/psweb/db" - "peerswap-web/cmd/psweb/internet" "peerswap-web/cmd/psweb/liquid" "peerswap-web/cmd/psweb/ps" @@ -46,6 +44,8 @@ var ( keyToNodeId = make(map[string]string) // public key of the sender of pegin_started broadcast ClaimJoinHandler string + // timestamp of the pegin_started broadcast of the current ClaimJoinHandler + ClaimJoinHandlerTS uint64 // when currently pending pegin can be claimed ClaimBlockHeight uint32 // time limit to join another claim @@ -60,8 +60,6 @@ var ( claimPSET string // count how many times tried to join, to give up after 10 joinCounter int - // flag that PSWeb is about to restart - restarting = false ) type Coordination struct { @@ -100,6 +98,7 @@ type ClaimParty struct { // runs after restart, to continue if pegin is ongoing func loadClaimJoinDB() { db.Load("ClaimJoin", "ClaimJoinHandler", &ClaimJoinHandler) + db.Load("ClaimJoin", "ClaimJoinHandlerTS", &ClaimJoinHandlerTS) db.Load("ClaimJoin", "ClaimBlockHeight", &ClaimBlockHeight) db.Load("ClaimJoin", "JoinBlockHeight", &JoinBlockHeight) db.Load("ClaimJoin", "ClaimStatus", &ClaimStatus) @@ -107,21 +106,21 @@ func loadClaimJoinDB() { db.Load("ClaimJoin", "MyRole", &MyRole) db.Load("ClaimJoin", "keyToNodeId", &keyToNodeId) - if MyRole == "initiator" { - db.Load("ClaimJoin", "claimPSET", &claimPSET) - db.Load("ClaimJoin", "claimParties", &ClaimParties) - - log.Println("Re-broadcasting own invitation") - InitiateClaimJoin(ClaimParties[0].ClaimBlockHeight) - } - if MyRole != "none" { var serializedKey []byte db.Load("ClaimJoin", "serializedPrivateKey", &serializedKey) myPrivateKey, _ = btcec.PrivKeyFromBytes(serializedKey) log.Println("Continue as ClaimJoin " + MyRole + " with pubKey " + MyPublicKey()) + + db.Load("ClaimJoin", "claimParties", &ClaimParties) + + if MyRole == "initiator" { + db.Load("ClaimJoin", "claimPSET", &claimPSET) + log.Println("Re-broadcasting own invitation") + InitiateClaimJoin(ClaimParties[0].ClaimBlockHeight) + } } else if ClaimJoinHandler != "" { - log.Println("Have ClaimJoin invite from", ClaimJoinHandler) + log.Println("Continue with ClaimJoin invite from", ClaimJoinHandler) } } @@ -167,10 +166,12 @@ create_pset: return } + total := strconv.Itoa(len(ClaimParties)) + signing := 0 + for i, output := range analyzed.Outputs { if output.Blind && output.Status == "unblinded" { blinder := decoded.Outputs[i].BlinderIndex - total := strconv.Itoa(len(ClaimParties)) ClaimStatus = "Blinding " + strconv.Itoa(i+1) + "/" + total if blinder == 0 { @@ -189,7 +190,8 @@ create_pset: if i == len(ClaimParties)-1 { // the final blinder can blind and sign at once action = "process2" - ClaimStatus += " & Signing " + strconv.Itoa(blinder) + "/" + total + ClaimStatus += " & Signing 1/" + total + signing++ } if checkPeerStatus(blinder) { @@ -222,8 +224,9 @@ create_pset: // Iterate through inputs in reverse order to sign for i := len(ClaimParties) - 1; i >= 0; i-- { input := decoded.Inputs[i] + signing++ if len(input.FinalScriptWitness) == 0 { - ClaimStatus = "Signing " + strconv.Itoa(len(ClaimParties)-i) + "/" + strconv.Itoa(len(ClaimParties)) + ClaimStatus = "Signing " + strconv.Itoa(signing) + "/" + total log.Println(ClaimStatus) if i == 0 { @@ -337,8 +340,8 @@ create_pset: func checkPeerStatus(i int) bool { ClaimParties[i].SentCount++ - if ClaimParties[i].SentCount > 3 { - // peer is offline, kick him + if ClaimParties[i].SentCount > 2 { + // peer is not responding, kick him kickPeer(ClaimParties[i].PubKey, "being unresponsive") return false } @@ -377,7 +380,7 @@ func Broadcast(fromNodeId string, message *Message) error { } } - if fromNodeId == myNodeId || (fromNodeId != myNodeId && (message.Asset == "pegin_started" && keyToNodeId[message.Sender] == "" || message.Asset == "pegin_ended" && ClaimJoinHandler != "")) { + if fromNodeId == myNodeId || (fromNodeId != myNodeId && (message.Asset == "pegin_started" && keyToNodeId[message.Sender] == "" || message.Asset == "pegin_ended" && keyToNodeId[message.Sender] != "")) { // forward to everyone else client, cleanup, err := ps.GetClient(config.Config.RpcHost) @@ -405,7 +408,7 @@ func Broadcast(fromNodeId string, message *Message) error { } } - if fromNodeId == myNodeId { + if fromNodeId == myNodeId || message.Sender == MyPublicKey() { // do nothing more if this is my own broadcast return nil } @@ -421,23 +424,16 @@ func Broadcast(fromNodeId string, message *Message) error { case "pegin_started": if MyRole == "initiator" { // two simultaneous initiators conflict, the earlier wins - if len(ClaimParties) > 1 || message.Amount > uint64(JoinBlockHeight) { + if len(ClaimParties) > 1 || ClaimJoinHandlerTS < message.TimeStamp { log.Println("Initiator collision, staying as initiator") // Re-broadcast own invitation after a 60-second delay - time.AfterFunc(60*time.Second, func() { + /* time.AfterFunc(60*time.Second, func() { log.Println("Re-broadcasting own invitation") InitiateClaimJoin(ClaimParties[0].ClaimBlockHeight) - }) + }) */ return nil - } else if message.Amount == uint64(JoinBlockHeight) { - // a tie, solved by random restart - EndClaimJoin("", "Initiator collision") - delay := mathRand.Intn(4)*10 + 10 - log.Printf("Initiator collision, switching to 'none' and restarting with %d seconds delay", delay) - restarting = true - time.Sleep(time.Duration(delay) * time.Second) - os.Exit(0) } else { + log.Println("Initiator collision, switching to 'none'") MyRole = "none" db.Save("ClaimJoin", "MyRole", MyRole) } @@ -449,25 +445,29 @@ func Broadcast(fromNodeId string, message *Message) error { if ClaimJoinHandler != message.Sender { // where to forward claimjoin request ClaimJoinHandler = message.Sender + // Save timestamp of the announcement + ClaimJoinHandlerTS = message.TimeStamp // Time limit to apply is communicated via Amount JoinBlockHeight = uint32(message.Amount) - ClaimStatus = "Received invitation to ClaimJoin" // reset counter of join attempts joinCounter = 0 - log.Println(ClaimStatus, "from", ClaimJoinHandler) + ClaimStatus = "Received invitation to ClaimJoin" + + log.Println(ClaimStatus, "from", ClaimJoinHandler, "via", GetAlias(fromNodeId)) // persist to db db.Save("ClaimJoin", "ClaimJoinHandler", ClaimJoinHandler) + db.Save("ClaimJoin", "ClaimJoinHandlerTS", ClaimJoinHandlerTS) db.Save("ClaimJoin", "JoinBlockHeight", JoinBlockHeight) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) } case "pegin_ended": // only trust the message from the original handler - if MyRole == "joiner" && ClaimJoinHandler == message.Sender && config.Config.PeginClaimScript != "done" { + if ClaimJoinHandler == message.Sender { txId := string(message.Payload) - if txId != "" { + if MyRole == "joiner" && txId != "" && config.Config.PeginClaimScript != "done" { ClaimStatus = "ClaimJoin pegin successful! Liquid TxId: " + txId // signal to telegram bot config.Config.PeginTxId = txId @@ -477,6 +477,10 @@ func Broadcast(fromNodeId string, message *Message) error { } log.Println(ClaimStatus) resetClaimJoin() + } else { + // forget the route only + keyToNodeId[message.Sender] = "" + db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) } } @@ -526,8 +530,14 @@ func SendCoordination(destinationPubKey string, message *Coordination) bool { // Either forward to final destination or decrypt and process func Process(message *Message, senderNodeId string) { + if myNodeId == "" { + // populates myNodeId + if getLndVersion() == 0 { + return + } + } - if keyToNodeId[message.Sender] != senderNodeId { + if keyToNodeId[message.Sender] != senderNodeId && senderNodeId != myNodeId { // save source key map keyToNodeId[message.Sender] = senderNodeId // persist to db @@ -565,13 +575,13 @@ func Process(message *Message, senderNodeId string) { return } - if ok, newClaimBlockHeight, status := addClaimParty(&msg.Joiner); ok { + if ok, status := addClaimParty(&msg.Joiner); ok { if SendCoordination(msg.Joiner.PubKey, &Coordination{ Action: "confirm_add", - ClaimBlockHeight: max(ClaimBlockHeight, newClaimBlockHeight), + ClaimBlockHeight: max(ClaimBlockHeight, msg.ClaimBlockHeight), Status: status, }) { - ClaimBlockHeight = max(ClaimBlockHeight, newClaimBlockHeight) + ClaimBlockHeight = max(ClaimBlockHeight, msg.ClaimBlockHeight) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) ClaimStatus = "Added new joiner, total participants: " + strconv.Itoa(len(ClaimParties)) @@ -766,11 +776,14 @@ func Process(message *Message, senderNodeId string) { Destination: message.Destination, Sender: MyPublicKey(), }) - return + // forget the pubKey forgetPubKey(message.Destination) + return } + // log.Println("Relaying", message.Memo, "from", GetAlias(senderNodeId), "to", GetAlias(destinationNodeId)) + err := SendCustomMessage(cl, destinationNodeId, message) if err != nil { log.Println("Cannot relay:", err) @@ -814,7 +827,9 @@ func InitiateClaimJoin(claimBlockHeight uint32) bool { } } - if len(ClaimParties) != 1 || ClaimParties[0].PubKey != MyPublicKey() { + // original invitation timestamp + ts := ClaimJoinHandlerTS + if MyRole == "none" && len(ClaimParties) != 1 || ClaimParties[0].PubKey != MyPublicKey() { party := createClaimParty(claimBlockHeight) if party != nil { // initiate array of claim parties @@ -825,26 +840,27 @@ func InitiateClaimJoin(claimBlockHeight uint32) bool { db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) db.Save("ClaimJoin", "JoinBlockHeight", JoinBlockHeight) db.Save("ClaimJoin", "claimParties", ClaimParties) + // new invitation timestamp + ts = uint64(time.Now().Unix()) } else { return false } } - if restarting { - // resolving initiator collision - return false - } - if Broadcast(myNodeId, &Message{ - Version: MessageVersion, - Memo: "broadcast", - Asset: "pegin_started", - Amount: uint64(JoinBlockHeight), - Sender: MyPublicKey(), + Version: MessageVersion, + Memo: "broadcast", + Asset: "pegin_started", + Amount: uint64(JoinBlockHeight), + Sender: MyPublicKey(), + TimeStamp: ts, }) == nil { if len(ClaimParties) == 1 { + // initial invite before everyone joined + ClaimJoinHandlerTS = ts ClaimStatus = "Invites sent, awaiting joiners" // persist to db + db.Save("ClaimJoin", "ClaimJoinHandlerTS", ClaimJoinHandlerTS) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) } return true @@ -1034,33 +1050,23 @@ func createClaimParty(claimBlockHeight uint32) *ClaimParty { } // add claim party to the list -func addClaimParty(newParty *ClaimParty) (bool, uint32, string) { - txHeight := uint32(internet.GetTxHeight(newParty.TxId)) - claimHight := txHeight + PeginBlocks - 1 - - // verify claimBlockHeight - if txHeight > 0 && claimHight != newParty.ClaimBlockHeight { - log.Printf("New joiner's ClaimBlockHeight was wrong: %d vs %d correct", newParty.ClaimBlockHeight, claimHight) - } else { - // cannot verify, have to trust the joiner - claimHight = newParty.ClaimBlockHeight - } +func addClaimParty(newParty *ClaimParty) (bool, string) { for _, party := range ClaimParties { if party.ClaimScript == newParty.ClaimScript { // is already in the list - return true, claimHight, "Successfully joined, total participants: " + strconv.Itoa(len(ClaimParties)) + return true, "Successfully joined, total participants: " + strconv.Itoa(len(ClaimParties)) } } if len(ClaimParties) == maxParties { - return false, claimHight, "Refuse to add, over limit of " + strconv.Itoa(maxParties) + return false, "Refuse to add, over limit of " + strconv.Itoa(maxParties) } // verify TxOutProof proof, err := bitcoin.GetTxOutProof(newParty.TxId) if err != nil { - return false, claimHight, "Refuse to add, TX not confirmed" + return false, "Refuse to add, TX not confirmed" } if proof != newParty.TxoutProof { @@ -1073,7 +1079,7 @@ func addClaimParty(newParty *ClaimParty) (bool, uint32, string) { // persist to db db.Save("ClaimJoin", "claimParties", ClaimParties) - return true, claimHight, "Successfully joined, total participants: " + strconv.Itoa(len(ClaimParties)) + return true, "Successfully joined, total participants: " + strconv.Itoa(len(ClaimParties)) } // remove claim party from the list by public key diff --git a/cmd/psweb/ln/common.go b/cmd/psweb/ln/common.go index 790d63a..1718914 100644 --- a/cmd/psweb/ln/common.go +++ b/cmd/psweb/ln/common.go @@ -149,10 +149,11 @@ type DataPoint struct { // sent/received as GOB type Message struct { // cleartext announcements - Version int - Memo string - Asset string - Amount uint64 + Version int + Memo string + Asset string + Amount uint64 + TimeStamp uint64 // encrypted communications via peer relay Sender string Destination string From 02039b15d380041628f97ad61687153a388e88f5 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Wed, 14 Aug 2024 16:43:53 +0200 Subject: [PATCH 21/98] Fix ClaimParties db case --- CHANGELOG.md | 6 +++--- cmd/psweb/ln/claimjoin.go | 27 +++++++++++++-------------- cmd/psweb/main.go | 4 ++-- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eb2917..054773a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,9 @@ ## 1.7.0 -- Implemented confidential joint pegin claims (ClaimJoin) -- Switched Custom Message serialization from JSON to GOB -- Switched Custom Message type from 42067 to 42065 +- Implement confidential joint pegin claims (Elements v23.2.2 & LND) +- Switch Custom Message serialization from JSON to GOB +- Switch Custom Message type from 42067 to 42065 - Better estimate swap-in fee and maximum swap amount - {TODO] Account for circular rebalancing cost and PPM diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 727e50a..6531648 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -55,7 +55,7 @@ var ( // none, initiator or joiner MyRole = "none" // array of initiator + joiners, for initiator only - ClaimParties []*ClaimParty + ClaimParties []ClaimParty // PSET to be blinded and signed by all parties claimPSET string // count how many times tried to join, to give up after 10 @@ -105,6 +105,7 @@ func loadClaimJoinDB() { db.Load("ClaimJoin", "MyRole", &MyRole) db.Load("ClaimJoin", "keyToNodeId", &keyToNodeId) + db.Load("ClaimJoin", "ClaimParties", &ClaimParties) if MyRole != "none" { var serializedKey []byte @@ -112,8 +113,6 @@ func loadClaimJoinDB() { myPrivateKey, _ = btcec.PrivKeyFromBytes(serializedKey) log.Println("Continue as ClaimJoin " + MyRole + " with pubKey " + MyPublicKey()) - db.Load("ClaimJoin", "claimParties", &ClaimParties) - if MyRole == "initiator" { db.Load("ClaimJoin", "claimPSET", &claimPSET) log.Println("Re-broadcasting own invitation") @@ -680,7 +679,7 @@ func Process(message *Message, senderNodeId string) { // remove yourself from ClaimJoin if SendCoordination(ClaimJoinHandler, &Coordination{ Action: "remove", - Joiner: *ClaimParties[0], + Joiner: ClaimParties[0], }) { // forget pegin handler, so that cannot initiate new ClaimJoin JoinBlockHeight = 0 @@ -834,12 +833,12 @@ func InitiateClaimJoin(claimBlockHeight uint32) bool { if party != nil { // initiate array of claim parties ClaimParties = nil - ClaimParties = append(ClaimParties, party) + ClaimParties = append(ClaimParties, *party) ClaimBlockHeight = claimBlockHeight JoinBlockHeight = claimBlockHeight - 1 db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) db.Save("ClaimJoin", "JoinBlockHeight", JoinBlockHeight) - db.Save("ClaimJoin", "claimParties", ClaimParties) + db.Save("ClaimJoin", "ClaimParties", ClaimParties) // new invitation timestamp ts = uint64(time.Now().Unix()) } else { @@ -909,7 +908,7 @@ func resetClaimJoin() { keyToNodeId = make(map[string]string) // persist to db - db.Save("ClaimJoin", "claimParties", ClaimParties) + db.Save("ClaimJoin", "ClaimParties", ClaimParties) db.Save("ClaimJoin", "ClaimJoinHandler", ClaimJoinHandler) db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) db.Save("ClaimJoin", "JoinBlockHeight", JoinBlockHeight) @@ -928,7 +927,7 @@ func JoinClaimJoin(claimBlockHeight uint32) bool { // no reply, remove yourself from ClaimJoin SendCoordination(ClaimJoinHandler, &Coordination{ Action: "remove", - Joiner: *ClaimParties[0], + Joiner: ClaimParties[0], }) forgetPubKey(ClaimJoinHandler) ClaimStatus = "Initator does not respond, forget him" @@ -983,7 +982,7 @@ func JoinClaimJoin(claimBlockHeight uint32) bool { if len(ClaimParties) != 1 || ClaimParties[0].PubKey != MyPublicKey() { // initiate array of claim parties for single entry ClaimParties = nil - ClaimParties = append(ClaimParties, createClaimParty(claimBlockHeight)) + ClaimParties = append(ClaimParties, *createClaimParty(claimBlockHeight)) ClaimBlockHeight = claimBlockHeight db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) db.Save("ClaimJoin", "ClaimParties", ClaimParties) @@ -991,7 +990,7 @@ func JoinClaimJoin(claimBlockHeight uint32) bool { if SendCoordination(ClaimJoinHandler, &Coordination{ Action: "add", - Joiner: *ClaimParties[0], + Joiner: ClaimParties[0], ClaimBlockHeight: claimBlockHeight, }) { // increment counter @@ -1074,17 +1073,17 @@ func addClaimParty(newParty *ClaimParty) (bool, string) { newParty.TxoutProof = proof } - ClaimParties = append(ClaimParties, newParty) + ClaimParties = append(ClaimParties, *newParty) // persist to db - db.Save("ClaimJoin", "claimParties", ClaimParties) + db.Save("ClaimJoin", "ClaimParties", ClaimParties) return true, "Successfully joined, total participants: " + strconv.Itoa(len(ClaimParties)) } // remove claim party from the list by public key func removeClaimParty(pubKey string) bool { - var newClaimParties []*ClaimParty + var newClaimParties []ClaimParty found := false claimBlockHeight := uint32(0) @@ -1110,7 +1109,7 @@ func removeClaimParty(pubKey string) bool { ClaimParties = newClaimParties // persist to db - db.Save("ClaimJoin", "claimParties", ClaimParties) + db.Save("ClaimJoin", "ClaimParties", ClaimParties) return true } diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index ac5a8bd..a30b393 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -1146,9 +1146,9 @@ func checkPegin() { // if no one has joined, switch after 1 extra block margin = 1 } - if currentBlockHeight > ln.ClaimBlockHeight+margin { + if currentBlockHeight >= ln.ClaimBlockHeight+margin { // claim pegin individually - t := "ClaimJoin failed, switching to individual claim" + t := "ClaimJoin failed, falling back to the individual claim" log.Println(t) telegramSendMessage("🧬 " + t) ln.MyRole = "none" From 0f1c00c77e3e4b364a9ea258eae26a7ab71bdd30 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Thu, 15 Aug 2024 15:36:29 +0200 Subject: [PATCH 22/98] Exact fee rate for LND --- .vscode/launch.json | 4 +-- CHANGELOG.md | 9 ++--- cmd/psweb/bitcoin/bitcoin.go | 22 ++++++++++++ cmd/psweb/handlers.go | 8 ++--- cmd/psweb/ln/claimjoin.go | 2 +- cmd/psweb/ln/cln.go | 4 +-- cmd/psweb/ln/lnd.go | 57 +++++++++++++++++++++++++++++- cmd/psweb/templates/bitcoin.gohtml | 14 +------- 8 files changed, 93 insertions(+), 27 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2c86c18..41901b4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,11 +9,11 @@ "type": "go", "request": "launch", "mode": "auto", - "buildFlags": "-tags lnd", + "buildFlags": "-tags cln", "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - "args": ["-datadir", "/home/vlad/.peerswap_t4"] + //"args": ["-datadir", "/home/vlad/.peerswap_t4"] //"args": ["-datadir", "/home/vlad/.peerswap2"] } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 054773a..9273a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,11 @@ ## 1.7.0 -- Implement confidential joint pegin claims (Elements v23.2.2 & LND) -- Switch Custom Message serialization from JSON to GOB -- Switch Custom Message type from 42067 to 42065 -- Better estimate swap-in fee and maximum swap amount +- LND: Implement confidential joint pegin claims (Elements v23.2.2 & LND) +- LND: exact fee rate on pegins and BTC withdrawals, RBF with 1 s/vb bump +- Switch Custom Message serialization from JSON to GOB (not compatible with old versions) +- Switch Custom Message type from 42067 to 42065 (not compatible with old versions) +- Better estimate for swap-in fees and maximum swap amounts - {TODO] Account for circular rebalancing cost and PPM ## 1.6.8 diff --git a/cmd/psweb/bitcoin/bitcoin.go b/cmd/psweb/bitcoin/bitcoin.go index a3be10d..9dba6fc 100644 --- a/cmd/psweb/bitcoin/bitcoin.go +++ b/cmd/psweb/bitcoin/bitcoin.go @@ -384,3 +384,25 @@ func GetFeeFromPsbt(psbtBytes *[]byte) (float64, error) { return fee, nil } + +func CreatePSBT(params interface{}) (string, error) { + + client := BitcoinClient() + service := &Bitcoin{client} + + r, err := service.client.call("createpsbt", params, "") + if err = handleError(err, &r); err != nil { + log.Printf("Failed to create PSET: %v", err) + return "", err + } + + var response string + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("CreatePSET unmarshall: %v", err) + return "", err + } + + return response, nil +} diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index a63d1f2..9051e5c 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -694,8 +694,8 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { fee = fee + fee/2 } } - if fee < config.Config.PeginFeeRate+2 { - fee = config.Config.PeginFeeRate + 2 // min increment + if fee < config.Config.PeginFeeRate+1 { + fee = config.Config.PeginFeeRate + 1 // min increment } } } @@ -709,7 +709,7 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { target := int32(ln.ClaimBlockHeight) maxConfs = target - bh + confs duration = time.Duration(10*(target-bh)) * time.Minute - } else { + } else if ln.ClaimJoinHandler != "" { cjETA = int((int32(ln.JoinBlockHeight) - bh + ln.PeginBlocks) / 6) } @@ -739,7 +739,7 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { MempoolFeeRate: mempoolFeeRate, LiquidFeeRate: liquid.EstimateFee(), SuggestedFeeRate: fee, - MinBumpFeeRate: config.Config.PeginFeeRate + 2, + MinBumpFeeRate: config.Config.PeginFeeRate + 1, CanBump: canBump, CanRBF: ln.CanRBF(), IsCLN: ln.Implementation == "CLN", diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 6531648..837575b 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -453,7 +453,7 @@ func Broadcast(fromNodeId string, message *Message) error { ClaimStatus = "Received invitation to ClaimJoin" - log.Println(ClaimStatus, "from", ClaimJoinHandler, "via", GetAlias(fromNodeId)) + log.Println(ClaimStatus, "from", ClaimJoinHandler) // , "via", GetAlias(fromNodeId)) // persist to db db.Save("ClaimJoin", "ClaimJoinHandler", ClaimJoinHandler) diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index 2a5e245..2ea77dd 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -712,14 +712,14 @@ func NewAddress() (string, error) { } err = client.Request(&glightning.NewAddrRequest{ - AddressType: "bech32", + AddressType: "p2tr", }, &res) if err != nil { log.Println("NewAddrRequest:", err) return "", err } - return res.Bech32, nil + return res.Taproot, nil } // Returns Lightning channels as peerswaprpc.ListPeersResponse, excluding private channels and certain nodes diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 170655d..3f3389a 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -318,6 +318,9 @@ func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate uint } } + firstPass := true + +finalize: // sign psbt res2, err := cl.FinalizePsbt(ctx, &walletrpc.FinalizePsbtRequest{ FundedPsbt: psbtBytes, @@ -330,6 +333,58 @@ func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate uint rawTx := res2.GetRawFinalTx() + decoded, err := bitcoin.DecodeRawTransaction(hex.EncodeToString(rawTx)) + if err != nil { + return nil, err + } + + feePaid, err := bitcoin.GetFeeFromPsbt(&psbtBytes) + if err != nil { + return nil, err + } + + requiredFee := int64(feeRate) * int64(decoded.VSize) + + if requiredFee != toSats(feePaid) { + if firstPass { + releaseOutputs(cl, utxos, &lockId) + + // Parse the PSBT + p, err := psbt.NewFromRawBytes(bytes.NewReader(psbtBytes), false) + if err != nil { + log.Println("NewFromRawBytes:", err) + return nil, err + } + + // Output index to amend + outputIndex := len(p.UnsignedTx.TxOut) - 1 + + // Replace the value of the output + p.UnsignedTx.TxOut[outputIndex].Value -= requiredFee - toSats(feePaid) + + // Serialize the PSBT back to raw bytes + var buf bytes.Buffer + err = p.Serialize(&buf) + if err != nil { + log.Println("Serialize:", err) + return nil, err + } + + psbtBytes = buf.Bytes() + + // Print the updated PSBT in hex + // log.Println(base64.StdEncoding.EncodeToString(psbtBytes)) + + // avoid permanent loop + firstPass = false + + goto finalize + } + + // did not fix in one pass, give up + log.Println("Unable to fix fee paid:", toSats(feePaid), "vs required:", requiredFee) + } + // Deserialize the transaction to get the transaction hash. msgTx := &wire.MsgTx{} txReader := bytes.NewReader(rawTx) @@ -1525,7 +1580,7 @@ func NewAddress() (string, error) { defer cleanup() res, err := client.NewAddress(ctx, &lnrpc.NewAddressRequest{ - Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH, + Type: lnrpc.AddressType_TAPROOT_PUBKEY, }) if err != nil { log.Println("NewAddress:", err) diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index 6cf492d..abf8d37 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -48,7 +48,7 @@
- +
@@ -398,14 +398,6 @@ // Function to execute after a delay function delayedCalculateTransactionFee() { - // unselect UTXOs - var fields = document.querySelectorAll('#select'); - fields.forEach(function(element) { - element.checked = false - document.getElementById(element.value).classList.remove('is-selected'); - }); - document.getElementById("unselectAll").style.visibility = "hidden"; - // Clear previous timer if it exists clearTimeout(timerId); @@ -541,10 +533,6 @@ } }); } else { - // reset peginAmount to selectedAmount - peginAmount = selectedAmount; - document.getElementById("peginAmount").value = selectedAmount; - // allow unselect all with one click document.getElementById("unselectAll").textContent = selectedUtxoCount; document.getElementById("unselectAll").style.visibility = "visible"; From a7b0ef01f06a198b44ac4941fa744b8c331e7b38 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Fri, 16 Aug 2024 00:21:36 +0200 Subject: [PATCH 23/98] Exact fee rate --- .vscode/launch.json | 2 +- cmd/psweb/bitcoin/bitcoin.go | 5 +- cmd/psweb/config/common.go | 2 +- cmd/psweb/handlers.go | 22 +++---- cmd/psweb/ln/cln.go | 99 ++++++++++++++++++++++-------- cmd/psweb/ln/common.go | 7 ++- cmd/psweb/ln/lnd.go | 43 +++++++------ cmd/psweb/templates/bitcoin.gohtml | 4 +- 8 files changed, 119 insertions(+), 65 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 41901b4..eab2a29 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "type": "go", "request": "launch", "mode": "auto", - "buildFlags": "-tags cln", + "buildFlags": "-tags lnd", "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", diff --git a/cmd/psweb/bitcoin/bitcoin.go b/cmd/psweb/bitcoin/bitcoin.go index 9dba6fc..7c1f070 100644 --- a/cmd/psweb/bitcoin/bitcoin.go +++ b/cmd/psweb/bitcoin/bitcoin.go @@ -2,7 +2,6 @@ package bitcoin import ( "bytes" - "encoding/base64" "encoding/json" "errors" "fmt" @@ -359,9 +358,7 @@ func SendRawTransaction(hexstring string) (string, error) { } // extracts Fee from PSBT -func GetFeeFromPsbt(psbtBytes *[]byte) (float64, error) { - base64string := base64.StdEncoding.EncodeToString(*psbtBytes) - +func GetFeeFromPsbt(base64string string) (float64, error) { client := BitcoinClient() service := &Bitcoin{client} diff --git a/cmd/psweb/config/common.go b/cmd/psweb/config/common.go index 0fef5d8..64d0438 100644 --- a/cmd/psweb/config/common.go +++ b/cmd/psweb/config/common.go @@ -36,7 +36,7 @@ type Configuration struct { PeginReplacedTxId string PeginAddress string PeginAmount int64 - PeginFeeRate uint32 + PeginFeeRate float64 PeginClaimJoin bool LightningDir string BitcoinHost string diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index 9051e5c..be9eaeb 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -654,11 +654,11 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { TargetConfirmations int32 Progress int32 Duration string - FeeRate uint32 + FeeRate float64 LiquidFeeRate float64 MempoolFeeRate float64 - SuggestedFeeRate uint32 - MinBumpFeeRate uint32 + SuggestedFeeRate float64 + MinBumpFeeRate float64 CanBump bool CanRBF bool IsCLN bool @@ -674,7 +674,7 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { } btcBalance := ln.ConfirmedWalletBalance(cl) - fee := uint32(mempoolFeeRate) + fee := float64(mempoolFeeRate) confs := int32(0) canBump := false canCPFP := false @@ -738,8 +738,8 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { FeeRate: config.Config.PeginFeeRate, MempoolFeeRate: mempoolFeeRate, LiquidFeeRate: liquid.EstimateFee(), - SuggestedFeeRate: fee, - MinBumpFeeRate: config.Config.PeginFeeRate + 1, + SuggestedFeeRate: math.Ceil(fee*1000) / 1000, + MinBumpFeeRate: math.Ceil((config.Config.PeginFeeRate+1)*1000) / 1000, CanBump: canBump, CanRBF: ln.CanRBF(), IsCLN: ln.Implementation == "CLN", @@ -772,7 +772,7 @@ func peginHandler(w http.ResponseWriter, r *http.Request) { return } - fee, err := strconv.ParseUint(r.FormValue("feeRate"), 10, 64) + fee, err := strconv.ParseFloat(r.FormValue("feeRate"), 64) if err != nil { redirectWithError(w, r, "/bitcoin?", err) return @@ -907,7 +907,7 @@ func peginHandler(w http.ResponseWriter, r *http.Request) { config.Config.PeginAmount = res.AmountSat config.Config.PeginTxId = res.TxId config.Config.PeginReplacedTxId = "" - config.Config.PeginFeeRate = uint32(fee) + config.Config.PeginFeeRate = res.ExactSatVb if err := config.Save(); err != nil { redirectWithError(w, r, "/bitcoin?", err) @@ -929,7 +929,7 @@ func bumpfeeHandler(w http.ResponseWriter, r *http.Request) { return } - fee, err := strconv.ParseUint(r.FormValue("feeRate"), 10, 64) + fee, err := strconv.ParseFloat(r.FormValue("feeRate"), 64) if err != nil { redirectWithError(w, r, "/bitcoin?", err) return @@ -966,7 +966,7 @@ func bumpfeeHandler(w http.ResponseWriter, r *http.Request) { } if ln.CanRBF() { - log.Println("RBF TxId:", res.TxId) + log.Println("RBF TxId:", res.TxId, "RawHex:", res.RawHex) config.Config.PeginReplacedTxId = config.Config.PeginTxId config.Config.PeginAmount = res.AmountSat config.Config.PeginTxId = res.TxId @@ -976,7 +976,7 @@ func bumpfeeHandler(w http.ResponseWriter, r *http.Request) { } // save the new rate, so the next bump cannot be lower - config.Config.PeginFeeRate = uint32(fee) + config.Config.PeginFeeRate = res.ExactSatVb if err := config.Save(); err != nil { redirectWithError(w, r, "/bitcoin?", err) diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index 2ea77dd..7211f04 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "log" + "math" "sort" "strconv" "strings" @@ -219,7 +220,7 @@ type UnreserveInputsResponse struct { Reservations []Reservation `json:"reservations"` } -func BumpPeginFee(newFeeRate uint64, label string) (*SentResult, error) { +func BumpPeginFee(newFeeRate float64, label string) (*SentResult, error) { client, clean, err := GetClient() if err != nil { @@ -306,8 +307,26 @@ func GetRawTransaction(client *glightning.Lightning, txid string) (string, error return tx.RawTx, nil } +type WithdrawRequest struct { + Destination string `json:"destination"` + Satoshi string `json:"satoshi"` + FeeRate string `json:"feerate,omitempty"` + MinConf uint16 `json:"minconf,omitempty"` + Utxos []string `json:"utxos,omitempty"` +} + +func (r WithdrawRequest) Name() string { + return "withdraw" +} + +type WithdrawResult struct { + Tx string `json:"tx"` + TxId string `json:"txid"` + PSBT string `json:"psbt"` +} + // utxos: ["txid:index", ....] -func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate uint64, subtractFeeFromAmount bool, label string) (*SentResult, error) { +func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate float64, subtractFeeFromAmount bool, label string) (*SentResult, error) { client, clean, err := GetClient() if err != nil { log.Println("GetClient:", err) @@ -336,42 +355,74 @@ func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate uint } minConf := uint16(1) - multiplier := uint64(1000) + multiplier := float64(1000) if !subtractFeeFromAmount && config.Config.Chain == "mainnet" { multiplier = 935 // better sets fee rate for pegin tx with change } - res, err := client.WithdrawWithUtxos( - addr, - &glightning.Sat{ - Value: uint64(amount), - SendAll: subtractFeeFromAmount, - }, - &glightning.FeeRate{ - Rate: uint(feeRate * multiplier), - }, - &minConf, - inputs) + amountStr := fmt.Sprintf("%d", amount) + if subtractFeeFromAmount { + amountStr = "all" + } + + var res WithdrawResult + err = client.Request(&WithdrawRequest{ + Destination: addr, + Satoshi: amountStr, + FeeRate: fmt.Sprint(uint(feeRate*multiplier)) + "perkb", + MinConf: minConf, + Utxos: *utxos, + }, &res) + if err != nil { + log.Println("WithdrawRequest:", err) + return nil, err + } + // The returned res.Tx is UNSIGNED, ignore it + var decoded bitcoin.Transaction + _, err := bitcoin.GetRawTransaction(res.TxId, &decoded) if err != nil { - log.Println("WithdrawWithUtxos:", err) return nil, err } amountSent := amount - if subtractFeeFromAmount { - decodedTx, err := bitcoin.DecodeRawTransaction(res.Tx) - if err == nil { - if len(decodedTx.Vout) == 1 { - amountSent = toSats(decodedTx.Vout[0].Value) - } + if subtractFeeFromAmount && len(decoded.Vout) == 1 { + amountSent = toSats(decoded.Vout[0].Value) + } + + /* this fails with Unsupported version number + feePaid, err := bitcoin.GetFeeFromPsbt(res.PSBT) + if err != nil { + return nil, err + } + */ + + // default value if exact calculation fails + feePaid := feeRate * float64(decoded.VSize) + + fee := int64(0) + for _, input := range decoded.Vin { + var decodedIn bitcoin.Transaction + _, err = bitcoin.GetRawTransaction(input.TXID, &decodedIn) + if err != nil { + goto return_result } + fee += toSats(decodedIn.Vout[input.Vout].Value) } + for _, output := range decoded.Vout { + fee -= toSats(output.Value) + } + + feePaid = float64(fee) + +return_result: + result := SentResult{ - RawHex: res.Tx, - TxId: res.TxId, - AmountSat: amountSent, + RawHex: decoded.Hex, + TxId: res.TxId, + AmountSat: amountSent, + ExactSatVb: math.Ceil(feePaid*1000/float64(decoded.VSize)) / 1000, } return &result, nil diff --git a/cmd/psweb/ln/common.go b/cmd/psweb/ln/common.go index 1718914..f2b1dc7 100644 --- a/cmd/psweb/ln/common.go +++ b/cmd/psweb/ln/common.go @@ -19,9 +19,10 @@ type UTXO struct { } type SentResult struct { - RawHex string - AmountSat int64 - TxId string + RawHex string + AmountSat int64 + TxId string + ExactSatVb float64 } type ForwardingStats struct { diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 3f3389a..1d9e4aa 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -13,6 +13,7 @@ import ( "errors" "fmt" "log" + "math" "os" "sort" "strconv" @@ -243,7 +244,7 @@ func GetRawTransaction(client lnrpc.LightningClient, txid string) (string, error } // utxos: ["txid:index", ....] -func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate uint64, subtractFeeFromAmount bool, label string) (*SentResult, error) { +func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate float64, subtractFeeFromAmount bool, label string) (*SentResult, error) { ctx := context.Background() conn, err := lndConnection() if err != nil { @@ -272,12 +273,12 @@ func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate uint // new template since LND 0.18+ // change lockID to custom and construct manual psbt lockId = myLockId - psbtBytes, err = fundPsbtSpendAll(cl, utxos, addr, feeRate) + psbtBytes, err = fundPsbtSpendAll(cl, utxos, addr, uint64(feeRate)) if err != nil { return nil, err } } else { - psbtBytes, err = fundPsbt(cl, utxos, outputs, feeRate) + psbtBytes, err = fundPsbt(cl, utxos, outputs, uint64(feeRate)) if err != nil { log.Println("FundPsbt:", err) return nil, err @@ -286,7 +287,7 @@ func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate uint if subtractFeeFromAmount { // trick for LND before 0.18 // replace output with correct address and amount - fee, err := bitcoin.GetFeeFromPsbt(&psbtBytes) + fee, err := bitcoin.GetFeeFromPsbt(base64.StdEncoding.EncodeToString(psbtBytes)) if err != nil { return nil, err } @@ -303,7 +304,7 @@ func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate uint } outputs[addr] = uint64(finalAmount) - psbtBytes, err = fundPsbt(cl, utxos, outputs, feeRate) + psbtBytes, err = fundPsbt(cl, utxos, outputs, uint64(feeRate)) if err == nil { // PFBT was funded successfully @@ -318,7 +319,7 @@ func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate uint } } - firstPass := true + pass := 0 finalize: // sign psbt @@ -338,15 +339,17 @@ finalize: return nil, err } - feePaid, err := bitcoin.GetFeeFromPsbt(&psbtBytes) + feePaid, err := bitcoin.GetFeeFromPsbt(base64.StdEncoding.EncodeToString(psbtBytes)) if err != nil { return nil, err } - requiredFee := int64(feeRate) * int64(decoded.VSize) + requiredFee := int64(feeRate * float64(decoded.VSize)) if requiredFee != toSats(feePaid) { - if firstPass { + if pass < 5 { + log.Println("Trying to fix fee paid:", toSats(feePaid), "vs required:", requiredFee) + releaseOutputs(cl, utxos, &lockId) // Parse the PSBT @@ -356,7 +359,7 @@ finalize: return nil, err } - // Output index to amend + // Output index to amend (destination or change) outputIndex := len(p.UnsignedTx.TxOut) - 1 // Replace the value of the output @@ -376,7 +379,7 @@ finalize: // log.Println(base64.StdEncoding.EncodeToString(psbtBytes)) // avoid permanent loop - firstPass = false + pass++ goto finalize } @@ -412,9 +415,10 @@ finalize: } result := SentResult{ - RawHex: hex.EncodeToString(rawTx), - TxId: msgTx.TxHash().String(), - AmountSat: finalAmount, + RawHex: hex.EncodeToString(rawTx), + TxId: msgTx.TxHash().String(), + AmountSat: finalAmount, + ExactSatVb: math.Ceil(float64(toSats(feePaid)*1000)/float64(decoded.VSize)) / 1000, } return &result, nil @@ -591,7 +595,7 @@ func fundPsbtSpendAll(cl walletrpc.WalletKitClient, utxoStrings *[]string, addre return fundResp.FundedPsbt, nil } -func BumpPeginFee(feeRate uint64, label string) (*SentResult, error) { +func BumpPeginFee(feeRate float64, label string) (*SentResult, error) { client, cleanup, err := GetClient() if err != nil { @@ -612,14 +616,15 @@ func BumpPeginFee(feeRate uint64, label string) (*SentResult, error) { cl := walletrpc.NewWalletKitClient(conn) if !CanRBF() { - err = doCPFP(cl, tx.GetOutputDetails(), feeRate) + err = doCPFP(cl, tx.GetOutputDetails(), uint64(feeRate)) if err != nil { return nil, err } else { return &SentResult{ - TxId: config.Config.PeginTxId, - RawHex: "", - AmountSat: config.Config.PeginAmount, + TxId: config.Config.PeginTxId, + RawHex: "", + AmountSat: config.Config.PeginAmount, + ExactSatVb: float64(feeRate), }, nil } } diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index abf8d37..353760a 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -64,7 +64,7 @@
- +
{{if .CanClaimJoin}} @@ -218,7 +218,7 @@

- +

From 689f3c45be06944da072974561e469035a54e8aa Mon Sep 17 00:00:00 2001 From: Impa10r Date: Sat, 17 Aug 2024 00:17:15 +0200 Subject: [PATCH 24/98] Update readme --- .vscode/launch.json | 2 +- CHANGELOG.md | 8 +-- README.md | 12 ++++- cmd/psweb/ln/claimjoin.go | 111 ++++++++++++++++++++------------------ cmd/psweb/ln/lnd.go | 6 +-- 5 files changed, 77 insertions(+), 62 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index eab2a29..2c86c18 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,7 @@ "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - //"args": ["-datadir", "/home/vlad/.peerswap_t4"] + "args": ["-datadir", "/home/vlad/.peerswap_t4"] //"args": ["-datadir", "/home/vlad/.peerswap2"] } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 9273a00..a945629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,10 @@ ## 1.7.0 -- LND: Implement confidential joint pegin claims (Elements v23.2.2 & LND) -- LND: exact fee rate on pegins and BTC withdrawals, RBF with 1 s/vb bump -- Switch Custom Message serialization from JSON to GOB (not compatible with old versions) -- Switch Custom Message type from 42067 to 42065 (not compatible with old versions) +- LND: Implement confidential joint pegin claims with Elements v23.2.2+ +- Record exact fee rates on pegins and BTC withdrawals for RBF with 1 s/vb bump +- Switch Custom Message serialization from JSON to GOB +- Switch Custom Message type from 42067 to 42065 - Better estimate for swap-in fees and maximum swap amounts - {TODO] Account for circular rebalancing cost and PPM diff --git a/README.md b/README.md index eebafee..5ef9470 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ sudo systemctl disable psweb # Liquid Pegin -Update: From v1.2.0 this is handled via UI on the Bitcoin page. +Update: Since v1.2.0 this is handled via UI on the Bitcoin page. To convert some BTC on your node into L-BTC you don't need any third party (but must run a full Bitcon node with txindex=1 enabled): @@ -215,6 +215,16 @@ alias ecli="docker exec -it elements_node_1 elements-cli -rpcuser=elements -rpcp (lookup Elements and Bitcoin rpc passwords in pswebconfig.com) +# Confidential Liquid Pegin + +Elements Core v23.2.2 introduced vsize discount for confidential transactions. Now sending a Liquid payment with a blinded amount costs the same or cheaper than a publicly visible (explicit) one. For example, claiming a pegin with ```elements-cli claimpegin``` costs about 45 sats, but it is possible to manually construct the same transaction with confidential destination address, blind and sign it, then post and only pay 36 sats. However, from privacy perspective, blinding a single pegin claim makes little sense. The linked Bitcoin UTXO will still show an explicit amount, so it is easily traceable to your new Liquid address. To achieve a truly confidential pegin, it is necessary to mix two or more independent claims into one single transaction, a-la CoinJoin. + +We implemented such "ClaimJoin" as an option for PSWeb. If you opt in when starting your pegin, your node will send invitations to all other PSWeb nodes to join in while you wait for your 102 confirmations. To join your claim, they should opt in as well while starting their own pegins. They won't know which node initiated the ClaimJoin and who else will be joining. The initiator also won't know which nodes responded. All communication happens blindly via fresh public/private key pairs. Nodes who do not directly participate act as blind relays of the messages, not being able to read them and not knowing the source and the final destination. This way the ClaimJoin coordination is not limited to direct peers. + +When all N pegins mature, the initiator node prepares one large PSET with N pegin inputs and N CT outputs, shuffled randomly, and sends it secuentially to all participants: first to blind their Liquid output and then sign their pegin input. Before blinding/signing and returning the PSET, each joiner verifies that his output address is there for the correct amount (allowing for upto 50 sats fee haircut). Up to 10 claims can be joined this way, to fit into one custom message (64kb). The price for additional privacy is time. For the initiator the wait can take upto 34 hours, while for the last joiner the same 17 hours as a single pegin. In practice, the blinding and signing round needs to be done twice: first to find out the exact discount vsize of the final transaction, then to pay the correct fee at 0.1 sat/vb. If the fee cannot be divided equally, the last participant pays slightnly more as a small incentive to join early. + +The process bears no risk to the participants. If any joiner becomes unresponsive during the blinding/signing round, it is automatically kicked out. If the initiator fails to complete the process, each joiner reverts to single pegin claim 10 blocks after the final maturity. As the last resort, you can always [claim your pegin manually](#liquid-pegin) with ```elements-cli```. All the necessary details are in PSWeb log. Your claim script and pegin transaction can only be used by your own Liquid wallet (private key), so there is no risk to share them. + # Support Information about PeerSwap and a link to join our Discord channel is at [PeerSwap.dev](https://peerswap.dev). Additionally, there is a [Telegram group](https://t.me/PeerSwapLN) for node runners with PeerSwap. Just beware of scammers who may DM you. Immediately block and report to Telegram anyone with empty Username field. diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 837575b..cd9f814 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -34,7 +34,7 @@ import ( // maximum number of participants in ClaimJoin const ( maxParties = 10 - PeginBlocks = 10 //102 + PeginBlocks = 2 //102 ) var ( @@ -193,25 +193,21 @@ create_pset: signing++ } - if checkPeerStatus(blinder) { - serializedPset, err := base64.StdEncoding.DecodeString(claimPSET) - if err != nil { - log.Println("Unable to serialize PSET") - return - } + serializedPset, err := base64.StdEncoding.DecodeString(claimPSET) + if err != nil { + log.Println("Unable to serialize PSET") + return + } - log.Println(ClaimStatus) + log.Println(ClaimStatus) - if !SendCoordination(ClaimParties[blinder].PubKey, &Coordination{ - Action: action, - PSET: serializedPset, - Status: ClaimStatus, - ClaimBlockHeight: ClaimBlockHeight, - }) { - log.Println("Unable to send coordination, cancelling ClaimJoin") - EndClaimJoin("", "Coordination failure") - return - } + if !SendCoordination(ClaimParties[blinder].PubKey, &Coordination{ + Action: action, + PSET: serializedPset, + Status: ClaimStatus, + ClaimBlockHeight: ClaimBlockHeight, + }) { + sendFailure(blinder, action) } db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) @@ -240,23 +236,21 @@ create_pset: log.Println(ClaimStatus) db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) } else { - if checkPeerStatus(i) { - serializedPset, err := base64.StdEncoding.DecodeString(claimPSET) - if err != nil { - log.Println("Unable to serialize PSET") - return - } + serializedPset, err := base64.StdEncoding.DecodeString(claimPSET) + if err != nil { + log.Println("Unable to serialize PSET") + return + } - if !SendCoordination(ClaimParties[i].PubKey, &Coordination{ - Action: "process", - PSET: serializedPset, - Status: ClaimStatus, - ClaimBlockHeight: ClaimBlockHeight, - }) { - log.Println("Unable to send blind coordination, cancelling ClaimJoin") - EndClaimJoin("", "Coordination failure") - } + if !SendCoordination(ClaimParties[i].PubKey, &Coordination{ + Action: "process", + PSET: serializedPset, + Status: ClaimStatus, + ClaimBlockHeight: ClaimBlockHeight, + }) { + sendFailure(i, "process") } + db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) return } @@ -337,19 +331,23 @@ create_pset: } } -func checkPeerStatus(i int) bool { - ClaimParties[i].SentCount++ - if ClaimParties[i].SentCount > 2 { +func sendFailure(peer int, action string) { + ClaimParties[peer].SentCount++ + ClaimStatus = "Failed to send coordiantion" + log.Printf("Failure #%d to send '%s' to %s", ClaimParties[peer].SentCount, action, ClaimParties[peer].PubKey) + if ClaimParties[peer].SentCount > 4 { // peer is not responding, kick him - kickPeer(ClaimParties[i].PubKey, "being unresponsive") - return false + kickPeer(ClaimParties[peer].PubKey, "being unresponsive") + if len(ClaimParties) < 2 { + log.Println("Unable to send coordination, cancelling ClaimJoin") + EndClaimJoin("", "Coordination failure") + } } - return true } func kickPeer(pubKey, reason string) { if ok := removeClaimParty(pubKey); ok { - ClaimStatus = "Joiner " + pubKey + " kicked, total participants: " + strconv.Itoa(len(ClaimParties)) + ClaimStatus = "Peer " + pubKey + " kicked, total participants: " + strconv.Itoa(len(ClaimParties)) // persist to db db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) log.Println(ClaimStatus) @@ -453,7 +451,7 @@ func Broadcast(fromNodeId string, message *Message) error { ClaimStatus = "Received invitation to ClaimJoin" - log.Println(ClaimStatus, "from", ClaimJoinHandler) // , "via", GetAlias(fromNodeId)) + log.Println(ClaimStatus, "from", ClaimJoinHandler, "via", GetAlias(fromNodeId)) // persist to db db.Save("ClaimJoin", "ClaimJoinHandler", ClaimJoinHandler) @@ -514,17 +512,24 @@ func SendCoordination(destinationPubKey string, message *Coordination) bool { cl, clean, er := GetClient() if er != nil { + log.Println("Cannot get lightning client:", err) return false } defer clean() - return SendCustomMessage(cl, destinationNodeId, &Message{ + err = SendCustomMessage(cl, destinationNodeId, &Message{ Version: MessageVersion, Memo: "process", Sender: MyPublicKey(), Destination: destinationPubKey, Payload: ciphertext, - }) == nil + }) + + if err != nil { + log.Println("Cannot send custom message:", err) + return false + } + return true } // Either forward to final destination or decrypt and process @@ -570,7 +575,7 @@ func Process(message *Message, senderNodeId string) { switch msg.Action { case "add": if MyRole != "initiator" { - log.Printf("Cannot add a joiner, not a claim initiator") + log.Printf("Cannot add a peer, not a claim initiator") return } @@ -583,7 +588,7 @@ func Process(message *Message, senderNodeId string) { ClaimBlockHeight = max(ClaimBlockHeight, msg.ClaimBlockHeight) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) - ClaimStatus = "Added new joiner, total participants: " + strconv.Itoa(len(ClaimParties)) + ClaimStatus = "Added new peer, total participants: " + strconv.Itoa(len(ClaimParties)) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) log.Println("Added "+msg.Joiner.PubKey+", total:", len(ClaimParties)) @@ -603,18 +608,18 @@ func Process(message *Message, senderNodeId string) { Action: "refuse_add", Status: status, }) { - log.Println("Refused new joiner: ", status) + log.Println("Refused new peer: ", status) } } case "remove": if MyRole != "initiator" { - log.Printf("Cannot remove a joiner, not a claim initiator") + log.Printf("Cannot remove a peer, not a claim initiator") return } if removeClaimParty(msg.Joiner.PubKey) { - ClaimStatus = "Removed a joiner, total participants: " + strconv.Itoa(len(ClaimParties)) + ClaimStatus = "Removed a peer, total participants: " + strconv.Itoa(len(ClaimParties)) // persist to db db.Save("ClaimJoin", "ClaimBlockHeight", ClaimBlockHeight) log.Println(ClaimStatus) @@ -633,7 +638,7 @@ func Process(message *Message, senderNodeId string) { } } } else { - log.Println("Cannot remove joiner, not in the list") + log.Println("Cannot remove peer, not in the list") } case "confirm_add": @@ -656,7 +661,7 @@ func Process(message *Message, senderNodeId string) { case "process2": // process twice to blind and sign if MyRole != "joiner" { - log.Println("received process2 while not being a joiner") + log.Println("received process2 while did not join") return } @@ -857,7 +862,7 @@ func InitiateClaimJoin(claimBlockHeight uint32) bool { if len(ClaimParties) == 1 { // initial invite before everyone joined ClaimJoinHandlerTS = ts - ClaimStatus = "Invites sent, awaiting joiners" + ClaimStatus = "Invites sent, awaiting peers to join" // persist to db db.Save("ClaimJoin", "ClaimJoinHandlerTS", ClaimJoinHandlerTS) db.Save("ClaimJoin", "ClaimStatus", ClaimStatus) @@ -1069,7 +1074,7 @@ func addClaimParty(newParty *ClaimParty) (bool, string) { } if proof != newParty.TxoutProof { - log.Printf("New joiner's TxoutProof was wrong") + log.Printf("New peer's TxoutProof was wrong") newParty.TxoutProof = proof } @@ -1284,7 +1289,7 @@ func savePrivateKey() { db.Save("ClaimJoin", "serializedPrivateKey", data) } -// checks that the output includes my address and amount +// checks that outputs include my address and amount func verifyPSET() bool { decoded, err := liquid.DecodePSET(claimPSET) if err != nil { diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 1d9e4aa..2b6bba5 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -348,7 +348,7 @@ finalize: if requiredFee != toSats(feePaid) { if pass < 5 { - log.Println("Trying to fix fee paid:", toSats(feePaid), "vs required:", requiredFee) + // log.Println("Trying to fix fee paid", toSats(feePaid), "vs required", requiredFee) releaseOutputs(cl, utxos, &lockId) @@ -384,8 +384,8 @@ finalize: goto finalize } - // did not fix in one pass, give up - log.Println("Unable to fix fee paid:", toSats(feePaid), "vs required:", requiredFee) + // did not fix in 5 passes, give up + log.Println("Unable to fix fee paid", toSats(feePaid), "vs required", requiredFee) } // Deserialize the transaction to get the transaction hash. From 81f102a1472d42344e3842af9da6aa32a2ec9ed0 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Sat, 17 Aug 2024 15:06:24 +0200 Subject: [PATCH 25/98] Fix relay --- README.md | 6 +++--- cmd/psweb/ln/claimjoin.go | 8 ++++---- cmd/psweb/ln/lnd.go | 2 ++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5ef9470..09f5226 100644 --- a/README.md +++ b/README.md @@ -217,11 +217,11 @@ alias ecli="docker exec -it elements_node_1 elements-cli -rpcuser=elements -rpcp # Confidential Liquid Pegin -Elements Core v23.2.2 introduced vsize discount for confidential transactions. Now sending a Liquid payment with a blinded amount costs the same or cheaper than a publicly visible (explicit) one. For example, claiming a pegin with ```elements-cli claimpegin``` costs about 45 sats, but it is possible to manually construct the same transaction with confidential destination address, blind and sign it, then post and only pay 36 sats. However, from privacy perspective, blinding a single pegin claim makes little sense. The linked Bitcoin UTXO will still show an explicit amount, so it is easily traceable to your new Liquid address. To achieve a truly confidential pegin, it is necessary to mix two or more independent claims into one single transaction, a-la CoinJoin. +Elements Core v23.2.2 introduced vsize discount for confidential transactions. Now sending a Liquid payment with a blinded amount costs the same or cheaper than a publicly visible (explicit) one. For example, claiming a pegin with ```elements-cli claimpegin``` costs about 45 sats, but it is possible to manually construct the same transaction (```elements-cli createrawtransaction```) with confidential destination address, blind and sign it, then post and only pay 36 sats. However, from privacy perspective, blinding a single pegin claim makes little sense. The linked Bitcoin UTXO will still show an explicit amount, so it is easily traceable to your new Liquid address. To achieve a truly confidential pegin, it is necessary to mix two or more independent claims into one single transaction, a-la CoinJoin. -We implemented such "ClaimJoin" as an option for PSWeb. If you opt in when starting your pegin, your node will send invitations to all other PSWeb nodes to join in while you wait for your 102 confirmations. To join your claim, they should opt in as well while starting their own pegins. They won't know which node initiated the ClaimJoin and who else will be joining. The initiator also won't know which nodes responded. All communication happens blindly via fresh public/private key pairs. Nodes who do not directly participate act as blind relays of the messages, not being able to read them and not knowing the source and the final destination. This way the ClaimJoin coordination is not limited to direct peers. +PSWeb implements such "ClaimJoin". If you opt in when starting your pegin, your node will send invitations to all other PSWeb nodes to join in while you wait for your 102 confirmations. To join your claim, they should opt in as well while starting their own pegins. They won't know which node initiated the ClaimJoin and who else will be joining. The initiator also won't know which nodes responded. All communication happens blindly via single use public/private key pairs (secp256k1). Nodes who do not directly participate act as relays of the messages, not being able to read them and not knowing the source and the final destination. This way the ClaimJoin coordination is not limited to direct peers. -When all N pegins mature, the initiator node prepares one large PSET with N pegin inputs and N CT outputs, shuffled randomly, and sends it secuentially to all participants: first to blind their Liquid output and then sign their pegin input. Before blinding/signing and returning the PSET, each joiner verifies that his output address is there for the correct amount (allowing for upto 50 sats fee haircut). Up to 10 claims can be joined this way, to fit into one custom message (64kb). The price for additional privacy is time. For the initiator the wait can take upto 34 hours, while for the last joiner the same 17 hours as a single pegin. In practice, the blinding and signing round needs to be done twice: first to find out the exact discount vsize of the final transaction, then to pay the correct fee at 0.1 sat/vb. If the fee cannot be divided equally, the last participant pays slightnly more as a small incentive to join early. +When all N pegins mature, the initiator node prepares one large PSET with N pegin inputs and N CT outputs, shuffled randomly, and sends it secuentially to all participants: first to blind Liquid outputs and then sign pegin inputs. Before blinding/signing and returning the PSET, each joiner verifies that his output address is there for the correct amount (allowing for upto 50 sats fee haircut). Upto 10 claims can be joined this way, to fit into one custom message (64kb). The price for additional privacy is time. For the initiator the wait can take upto 34 hours if the finel peers joins at block 101. While for that last joiner the wait will be the same 17 hours as a single pegin. In practice, the blinding and signing round needs to be done twice: first to find out the exact discount vsize of the final transaction, then to pay the correct fee at 0.1 sat/vb. If the fee cannot be divided equally, the last participant pays slightnly more as an incentive to join early. The process bears no risk to the participants. If any joiner becomes unresponsive during the blinding/signing round, it is automatically kicked out. If the initiator fails to complete the process, each joiner reverts to single pegin claim 10 blocks after the final maturity. As the last resort, you can always [claim your pegin manually](#liquid-pegin) with ```elements-cli```. All the necessary details are in PSWeb log. Your claim script and pegin transaction can only be used by your own Liquid wallet (private key), so there is no risk to share them. diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index cd9f814..2c170bf 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -34,7 +34,7 @@ import ( // maximum number of participants in ClaimJoin const ( maxParties = 10 - PeginBlocks = 2 //102 + PeginBlocks = 10 //102 ) var ( @@ -411,7 +411,7 @@ func Broadcast(fromNodeId string, message *Message) error { } // store for relaying further encrypted messages - if keyToNodeId[message.Sender] != fromNodeId { + if keyToNodeId[message.Sender] == "" { keyToNodeId[message.Sender] = fromNodeId db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) } @@ -541,7 +541,7 @@ func Process(message *Message, senderNodeId string) { } } - if keyToNodeId[message.Sender] != senderNodeId && senderNodeId != myNodeId { + if keyToNodeId[message.Sender] == "" && senderNodeId != myNodeId { // save source key map keyToNodeId[message.Sender] = senderNodeId // persist to db @@ -786,7 +786,7 @@ func Process(message *Message, senderNodeId string) { return } - // log.Println("Relaying", message.Memo, "from", GetAlias(senderNodeId), "to", GetAlias(destinationNodeId)) + log.Println("Relaying", message.Memo, "from", GetAlias(senderNodeId), "to", GetAlias(destinationNodeId)) err := SendCustomMessage(cl, destinationNodeId, message) if err != nil { diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 2b6bba5..5f9977b 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -1387,6 +1387,8 @@ func SendCustomMessage(client lnrpc.LightningClient, peerId string, message *Mes return err } + log.Printf("Sent %d bytes %s to %s", len(req.Data), message.Memo, GetAlias(peerId)) + return nil } From c488cfd8f43297b0ea7f24f959cb1b29729b5156 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Sat, 17 Aug 2024 15:10:45 +0200 Subject: [PATCH 26/98] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 09f5226..bb45504 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ alias ecli="docker exec -it elements_node_1 elements-cli -rpcuser=elements -rpcp Elements Core v23.2.2 introduced vsize discount for confidential transactions. Now sending a Liquid payment with a blinded amount costs the same or cheaper than a publicly visible (explicit) one. For example, claiming a pegin with ```elements-cli claimpegin``` costs about 45 sats, but it is possible to manually construct the same transaction (```elements-cli createrawtransaction```) with confidential destination address, blind and sign it, then post and only pay 36 sats. However, from privacy perspective, blinding a single pegin claim makes little sense. The linked Bitcoin UTXO will still show an explicit amount, so it is easily traceable to your new Liquid address. To achieve a truly confidential pegin, it is necessary to mix two or more independent claims into one single transaction, a-la CoinJoin. -PSWeb implements such "ClaimJoin". If you opt in when starting your pegin, your node will send invitations to all other PSWeb nodes to join in while you wait for your 102 confirmations. To join your claim, they should opt in as well while starting their own pegins. They won't know which node initiated the ClaimJoin and who else will be joining. The initiator also won't know which nodes responded. All communication happens blindly via single use public/private key pairs (secp256k1). Nodes who do not directly participate act as relays of the messages, not being able to read them and not knowing the source and the final destination. This way the ClaimJoin coordination is not limited to direct peers. +PSWeb implements such "ClaimJoin". If you opt in when starting your pegin, your node will send invitations to all other PSWeb nodes to join in while you wait for your 102 confirmations. To join your claim, they should opt in as well while starting their own pegins. They won't know which node initiated the ClaimJoin and who else will be joining. The initiator also won't know which nodes responded. All communication happens blindly via single use public/private key pairs (secp256k1). Nodes who do not directly participate act as relays for the encrypted messages, not being able to read them and not knowing the source and the final destination. This way our ClaimJoin coordination is fully confidential and not limited to direct peers. When all N pegins mature, the initiator node prepares one large PSET with N pegin inputs and N CT outputs, shuffled randomly, and sends it secuentially to all participants: first to blind Liquid outputs and then sign pegin inputs. Before blinding/signing and returning the PSET, each joiner verifies that his output address is there for the correct amount (allowing for upto 50 sats fee haircut). Upto 10 claims can be joined this way, to fit into one custom message (64kb). The price for additional privacy is time. For the initiator the wait can take upto 34 hours if the finel peers joins at block 101. While for that last joiner the wait will be the same 17 hours as a single pegin. In practice, the blinding and signing round needs to be done twice: first to find out the exact discount vsize of the final transaction, then to pay the correct fee at 0.1 sat/vb. If the fee cannot be divided equally, the last participant pays slightnly more as an incentive to join early. From 3d8d6522112f04f5799047f18e42cf62fb64b129 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Sat, 17 Aug 2024 15:13:21 +0200 Subject: [PATCH 27/98] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb45504..4da7ecb 100644 --- a/README.md +++ b/README.md @@ -227,4 +227,4 @@ The process bears no risk to the participants. If any joiner becomes unresponsiv # Support -Information about PeerSwap and a link to join our Discord channel is at [PeerSwap.dev](https://peerswap.dev). Additionally, there is a [Telegram group](https://t.me/PeerSwapLN) for node runners with PeerSwap. Just beware of scammers who may DM you. Immediately block and report to Telegram anyone with empty Username field. +Information about PeerSwap and a link to join their Discord channel is at [PeerSwap.dev](https://peerswap.dev). Additionally, there is a [Telegram group](https://t.me/PeerSwapLN) for node runners with PeerSwap. Just beware of scammers who may DM you. Immediately block and report to Telegram anyone with empty Username field. From ec9aa37120abecd0fb724f5a997ceb80d55f9527 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Sat, 17 Aug 2024 15:19:01 +0200 Subject: [PATCH 28/98] Update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4da7ecb..dc0e8c8 100644 --- a/README.md +++ b/README.md @@ -217,9 +217,9 @@ alias ecli="docker exec -it elements_node_1 elements-cli -rpcuser=elements -rpcp # Confidential Liquid Pegin -Elements Core v23.2.2 introduced vsize discount for confidential transactions. Now sending a Liquid payment with a blinded amount costs the same or cheaper than a publicly visible (explicit) one. For example, claiming a pegin with ```elements-cli claimpegin``` costs about 45 sats, but it is possible to manually construct the same transaction (```elements-cli createrawtransaction```) with confidential destination address, blind and sign it, then post and only pay 36 sats. However, from privacy perspective, blinding a single pegin claim makes little sense. The linked Bitcoin UTXO will still show an explicit amount, so it is easily traceable to your new Liquid address. To achieve a truly confidential pegin, it is necessary to mix two or more independent claims into one single transaction, a-la CoinJoin. +Elements Core v23.2.2 introduced vsize discount for confidential transactions. Now sending a Liquid payment with a blinded amount costs the same or cheaper than a publicly visible (explicit) one. For example, claiming a pegin with ```elements-cli claimpegin``` costs about 45 sats, but it is possible to manually construct the same transaction (```elements-cli createrawtransaction```) with confidential destination address, blind and sign it, then post and only pay 36 sats. However, from privacy perspective, blinding a single pegin claim makes little sense. The linked Bitcoin UTXO will still show the explicit amount, so it is easily traceable to your new Liquid address. To achieve a truly confidential pegin, it is necessary to mix two or more independent claims into one single transaction, a-la CoinJoin. -PSWeb implements such "ClaimJoin". If you opt in when starting your pegin, your node will send invitations to all other PSWeb nodes to join in while you wait for your 102 confirmations. To join your claim, they should opt in as well while starting their own pegins. They won't know which node initiated the ClaimJoin and who else will be joining. The initiator also won't know which nodes responded. All communication happens blindly via single use public/private key pairs (secp256k1). Nodes who do not directly participate act as relays for the encrypted messages, not being able to read them and not knowing the source and the final destination. This way our ClaimJoin coordination is fully confidential and not limited to direct peers. +In v1.7.0 PSWeb implemented such "ClaimJoin". If you opt in when starting your pegin, your node will send invitations to all other PSWeb nodes to join in while you wait for your 102 confirmations. To join your claim, they should opt in as well while starting their own pegins. They won't know which node initiated the ClaimJoin and who else will be joining. The initiator also won't know which nodes responded. All communication happens blindly via single use public/private key pairs (secp256k1). Nodes who do not directly participate act as relays for the encrypted messages, not being able to read them and not knowing the source and the final destination. This way our ClaimJoin coordination is fully confidential and not limited to direct peers. When all N pegins mature, the initiator node prepares one large PSET with N pegin inputs and N CT outputs, shuffled randomly, and sends it secuentially to all participants: first to blind Liquid outputs and then sign pegin inputs. Before blinding/signing and returning the PSET, each joiner verifies that his output address is there for the correct amount (allowing for upto 50 sats fee haircut). Upto 10 claims can be joined this way, to fit into one custom message (64kb). The price for additional privacy is time. For the initiator the wait can take upto 34 hours if the finel peers joins at block 101. While for that last joiner the wait will be the same 17 hours as a single pegin. In practice, the blinding and signing round needs to be done twice: first to find out the exact discount vsize of the final transaction, then to pay the correct fee at 0.1 sat/vb. If the fee cannot be divided equally, the last participant pays slightnly more as an incentive to join early. From 810ebf29c93f7aa28849df89dbc76604be23a920 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Sun, 18 Aug 2024 15:31:21 +0200 Subject: [PATCH 29/98] Fix insufficient fee for RBF --- .vscode/launch.json | 2 +- README.md | 2 +- cmd/psweb/handlers.go | 15 +++++++-- cmd/psweb/liquid/elements.go | 47 ++++++++++++++++++++++++++ cmd/psweb/ln/claimjoin.go | 54 ++++++++++++++++++++---------- cmd/psweb/ln/cln.go | 27 +++++---------- cmd/psweb/ln/lnd.go | 34 +++++++++++++------ cmd/psweb/templates/bitcoin.gohtml | 42 +++++++++++++---------- 8 files changed, 153 insertions(+), 70 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2c86c18..f7481a7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,7 @@ "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - "args": ["-datadir", "/home/vlad/.peerswap_t4"] + //"args": ["-datadir", "/home/vlad/.peerswap2_t4"] //"args": ["-datadir", "/home/vlad/.peerswap2"] } ] diff --git a/README.md b/README.md index dc0e8c8..c6446fc 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ alias ecli="docker exec -it elements_node_1 elements-cli -rpcuser=elements -rpcp (lookup Elements and Bitcoin rpc passwords in pswebconfig.com) -# Confidential Liquid Pegin +## Confidential Liquid Pegin Elements Core v23.2.2 introduced vsize discount for confidential transactions. Now sending a Liquid payment with a blinded amount costs the same or cheaper than a publicly visible (explicit) one. For example, claiming a pegin with ```elements-cli claimpegin``` costs about 45 sats, but it is possible to manually construct the same transaction (```elements-cli createrawtransaction```) with confidential destination address, blind and sign it, then post and only pay 36 sats. However, from privacy perspective, blinding a single pegin claim makes little sense. The linked Bitcoin UTXO will still show the explicit amount, so it is easily traceable to your new Liquid address. To achieve a truly confidential pegin, it is necessary to mix two or more independent claims into one single transaction, a-la CoinJoin. diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index be9eaeb..70e149d 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -738,8 +738,8 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { FeeRate: config.Config.PeginFeeRate, MempoolFeeRate: mempoolFeeRate, LiquidFeeRate: liquid.EstimateFee(), - SuggestedFeeRate: math.Ceil(fee*1000) / 1000, - MinBumpFeeRate: math.Ceil((config.Config.PeginFeeRate+1)*1000) / 1000, + SuggestedFeeRate: math.Ceil(fee*10) / 10, + MinBumpFeeRate: math.Ceil((config.Config.PeginFeeRate+1)*10) / 10, CanBump: canBump, CanRBF: ln.CanRBF(), IsCLN: ln.Implementation == "CLN", @@ -828,6 +828,17 @@ func peginHandler(w http.ResponseWriter, r *http.Request) { claimScript := "" if isPegin { + // check that elements is fully synced + info, err := liquid.GetBlockchainInfo() + if err != nil { + redirectWithError(w, r, "/bitcoin?", err) + return + } + if info.InitialBlockDownload { + redirectWithError(w, r, "/bitcoin?", errors.New("Elements Core initial block download is not done")) + return + } + // test on a pre-existing tx that bitcon core can complete the peg tx := "b61ec844027ce18fd3eb91fa7bed8abaa6809c4d3f6cf4952b8ebaa7cd46583a" if config.Config.Chain == "testnet" { diff --git a/cmd/psweb/liquid/elements.go b/cmd/psweb/liquid/elements.go index f0b4464..be106c8 100644 --- a/cmd/psweb/liquid/elements.go +++ b/cmd/psweb/liquid/elements.go @@ -796,3 +796,50 @@ func GetAddressInfo(addr, wallet string) (*AddressInfo, error) { return &response, nil } + +type BlockchainInfo struct { + Chain string `json:"chain"` + Blocks int `json:"blocks"` + Headers int `json:"headers"` + BestBlockHash string `json:"bestblockhash"` + Time int64 `json:"time"` + Mediantime int64 `json:"mediantime"` + VerificationProgress float64 `json:"verificationprogress"` + InitialBlockDownload bool `json:"initialblockdownload"` + SizeOnDisk int64 `json:"size_on_disk"` + Pruned bool `json:"pruned"` + CurrentParamsRoot string `json:"current_params_root"` + CurrentSignblockAsm string `json:"current_signblock_asm"` + CurrentSignblockHex string `json:"current_signblock_hex"` + MaxBlockWitness int `json:"max_block_witness"` + CurrentFedpegProgram string `json:"current_fedpeg_program"` + CurrentFedpegScript string `json:"current_fedpeg_script"` + ExtensionSpace []string `json:"extension_space"` + EpochLength int `json:"epoch_length"` + TotalValidEpochs int `json:"total_valid_epochs"` + EpochAge int `json:"epoch_age"` + Warnings string `json:"warnings"` +} + +// returns block hash +func GetBlockchainInfo() (*BlockchainInfo, error) { + client := ElementsClient() + service := &Elements{client} + params := &[]interface{}{} + + r, err := service.client.call("getblockchaininfo", params, "") + if err = handleError(err, &r); err != nil { + log.Printf("GetBlockchainInfo: %v", err) + return nil, err + } + + var response BlockchainInfo + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("GetBlockchainInfo unmarshall: %v", err) + return nil, err + } + + return &response, nil +} diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 2c170bf..857d326 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -34,7 +34,7 @@ import ( // maximum number of participants in ClaimJoin const ( maxParties = 10 - PeginBlocks = 10 //102 + PeginBlocks = 2 //102 ) var ( @@ -111,7 +111,7 @@ func loadClaimJoinDB() { var serializedKey []byte db.Load("ClaimJoin", "serializedPrivateKey", &serializedKey) myPrivateKey, _ = btcec.PrivKeyFromBytes(serializedKey) - log.Println("Continue as ClaimJoin " + MyRole + " with pubKey " + MyPublicKey()) + log.Println("Continue as ClaimJoin", MyRole, MyPublicKey()) if MyRole == "initiator" { db.Load("ClaimJoin", "claimPSET", &claimPSET) @@ -548,7 +548,7 @@ func Process(message *Message, senderNodeId string) { db.Save("ClaimJoin", "keyToNodeId", keyToNodeId) } - if message.Destination == MyPublicKey() && config.Config.PeginClaimJoin { + if message.Destination == MyPublicKey() && config.Config.PeginClaimJoin && len(ClaimParties) > 0 { // Decrypt the message using my private key plaintext, err := eciesDecrypt(myPrivateKey, message.Payload) if err != nil { @@ -570,7 +570,7 @@ func Process(message *Message, senderNodeId string) { return } - claimPSET = base64.StdEncoding.EncodeToString(msg.PSET) + newClaimPSET := base64.StdEncoding.EncodeToString(msg.PSET) switch msg.Action { case "add": @@ -666,7 +666,7 @@ func Process(message *Message, senderNodeId string) { } // process my output - claimPSET, _, err = liquid.ProcessPSET(claimPSET, config.Config.ElementsWallet) + newClaimPSET, _, err = liquid.ProcessPSET(newClaimPSET, config.Config.ElementsWallet) if err != nil { log.Println("Unable to process PSET:", err) return @@ -674,7 +674,7 @@ func Process(message *Message, senderNodeId string) { fallthrough // continue to second pass case "process": // blind or sign - if !verifyPSET() { + if !verifyPSET(newClaimPSET) { log.Println("PSET verification failure!") if MyRole == "initiator" { // kick the joiner who returned broken PSET @@ -696,10 +696,6 @@ func Process(message *Message, senderNodeId string) { db.Save("ClaimJoin", "MyRole", &MyRole) db.Save("ClaimJoin", "ClaimStatus", &ClaimStatus) db.Save("ClaimJoin", "ClaimJoinHandler", &ClaimJoinHandler) - - // disable ClaimJoin - config.Config.PeginClaimJoin = false - config.Save() } } return @@ -726,7 +722,7 @@ func Process(message *Message, senderNodeId string) { } if MyRole != "joiner" { - log.Println("received 'process' unexpected") + log.Println("Received 'process' unexpected") return } @@ -1289,28 +1285,50 @@ func savePrivateKey() { db.Save("ClaimJoin", "serializedPrivateKey", data) } -// checks that outputs include my address and amount -func verifyPSET() bool { - decoded, err := liquid.DecodePSET(claimPSET) +// checks that the new PSET has the same input/output count +// checks that outputs include my address and correct amount +func verifyPSET(newClaimPSET string) bool { + decodedNew, err := liquid.DecodePSET(newClaimPSET) if err != nil { return false } + if MyRole == "Initiator" { + decodedOld, err := liquid.DecodePSET(claimPSET) + if err != nil { + return false + } + + if decodedOld.InputCount != decodedNew.InputCount { + log.Println("PSET verification failed: wrong InputCount") + return false + } + + if decodedOld.OutputCount != decodedNew.OutputCount { + log.Println("PSET verification failed: wrong OutputCount") + return false + } + } + addressInfo, err := liquid.GetAddressInfo(ClaimParties[0].Address, config.Config.ElementsWallet) if err != nil { return false } - for _, output := range decoded.Outputs { + ok := false + for _, output := range decodedNew.Outputs { // 50 sats maximum fee allowed if output.Script.Address == addressInfo.Unconfidential && liquid.ToBitcoin(ClaimParties[0].Amount)-output.Amount < 0.0000005 { - return true + ok = true } } - log.Println(ClaimParties[0].Address, addressInfo.Unconfidential, ClaimParties[0].Amount) - log.Println(claimPSET) + if ok { + claimPSET = newClaimPSET + return true + } + log.Println("PSET verification failed: output address not found or insufficient amount") return false } diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index 7211f04..02d94c6 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -17,7 +17,6 @@ import ( "peerswap-web/cmd/psweb/bitcoin" "peerswap-web/cmd/psweb/config" - "peerswap-web/cmd/psweb/internet" "github.com/btcsuite/btcd/chaincfg" "github.com/elementsproject/glightning/glightning" @@ -154,24 +153,14 @@ func GetBlockHeight(client *glightning.Lightning) uint32 { // returns number of confirmations and whether the tx can be fee bumped func GetTxConfirmations(client *glightning.Lightning, txid string) (int32, bool) { - blockHeight := GetBlockHeight(client) - if blockHeight == 0 { - return 0, false - } - - height := internet.GetTxHeight(txid) - if height == 0 { - // mempool api error, use bitcoin core - var result bitcoin.Transaction - _, err := bitcoin.GetRawTransaction(txid, &result) - if err != nil { - return -1, true // signal tx not found - } - - return result.Confirmations, true + var tx bitcoin.Transaction + _, err := bitcoin.GetRawTransaction(txid, &tx) + if err != nil { + return -1, false // signal tx not found } - return int32(blockHeight) - height, true + + return tx.Confirmations, true } func GetAlias(nodeKey string) string { @@ -378,9 +367,9 @@ func SendCoinsWithUtxos(utxos *[]string, addr string, amount int64, feeRate floa return nil, err } - // The returned res.Tx is UNSIGNED, ignore it + // The returned res.Tx is UNSIGNED, ignore it and get new var decoded bitcoin.Transaction - _, err := bitcoin.GetRawTransaction(res.TxId, &decoded) + _, err = bitcoin.GetRawTransaction(res.TxId, &decoded) if err != nil { return nil, err } diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index 5f9977b..8591f87 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -348,7 +348,7 @@ finalize: if requiredFee != toSats(feePaid) { if pass < 5 { - // log.Println("Trying to fix fee paid", toSats(feePaid), "vs required", requiredFee) + log.Println("Trying to fix fee paid", toSats(feePaid), "vs required", requiredFee) releaseOutputs(cl, utxos, &lockId) @@ -648,18 +648,30 @@ func BumpPeginFee(feeRate float64, label string) (*SentResult, error) { utxos = append(utxos, input.Outpoint) } - // sometimes remove transaction is not enough - releaseOutputs(cl, &utxos, &internalLockId) - releaseOutputs(cl, &utxos, &myLockId) + var ress *SentResult + var errr error - return SendCoinsWithUtxos( - &utxos, - config.Config.PeginAddress, - config.Config.PeginAmount, - feeRate, - len(tx.OutputDetails) == 1, - label) + // extra bump may be necessary if the tx does not pay enough fee + for extraBump := float64(0); extraBump <= 1; extraBump += 0.1 { + // sometimes remove transaction is not enough + releaseOutputs(cl, &utxos, &internalLockId) + releaseOutputs(cl, &utxos, &myLockId) + ress, errr = SendCoinsWithUtxos( + &utxos, + config.Config.PeginAddress, + config.Config.PeginAmount, + feeRate+extraBump, + len(tx.OutputDetails) == 1, + label) + if errr != nil && errr.Error() == "rpc error: code = Unknown desc = insufficient fee" { + continue + } else { + break + } + } + + return ress, errr } func doCPFP(cl walletrpc.WalletKitClient, outputs []*lnrpc.OutputDetail, newFeeRate uint64) error { diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index 353760a..33f56cf 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -141,17 +141,21 @@ Confs:

- {{.Confirmations}} - {{if .IsPegin}} - / {{.TargetConfirmations}} -
- ETA: - - {{.Duration}} + {{if eq .Confirmations -1}} + Transaction not found in local mempool + {{else}} + {{.Confirmations}} + {{if .IsPegin}} + / {{.TargetConfirmations}} +
+ ETA: + + {{.Duration}} + {{end}} {{end}}
- TxId: {{.TxId}} +

TxId: {{.TxId}}

+
+
diff --git a/cmd/psweb/templates/peer.gohtml b/cmd/psweb/templates/peer.gohtml index 39da077..6049014 100644 --- a/cmd/psweb/templates/peer.gohtml +++ b/cmd/psweb/templates/peer.gohtml @@ -142,13 +142,19 @@ function fromChanged() { if (document.getElementById("from").value == "ln") { document.getElementById("to").value = "lbtc"; - document.getElementById("swapAmount").value = {{.RecommendLiquidSwapOut}}; + if ({{.RecommendLiquidSwapOut}} > 0) { + document.getElementById("swapAmount").value = {{.RecommendLiquidSwapOut}}; + } } else { document.getElementById("to").value = "ln"; if (document.getElementById("from").value == "btc") { - document.getElementById("swapAmount").value = {{.RecommendBitcoinSwapIn}}; + if ({{.RecommendBitcoinSwapIn}} > 0) { + document.getElementById("swapAmount").value = {{.RecommendBitcoinSwapIn}}; + } } else { - document.getElementById("swapAmount").value = {{.RecommendLiquidSwapIn}}; + if ({{.RecommendLiquidSwapIn}} > 0) { + document.getElementById("swapAmount").value = {{.RecommendLiquidSwapIn}}; + } } } calculateTransactionFee(); @@ -157,13 +163,19 @@ function toChanged() { if (document.getElementById("to").value == "ln") { document.getElementById("from").value = "lbtc"; - document.getElementById("swapAmount").value = {{.RecommendLiquidSwapIn}}; + if ({{.RecommendLiquidSwapIn}} > 0) { + document.getElementById("swapAmount").value = {{.RecommendLiquidSwapIn}}; + } } else { document.getElementById("from").value = "ln"; if (document.getElementById("to").value == "btc") { - document.getElementById("swapAmount").value = {{.RecommendBitcoinSwapOut}}; + if ({{.RecommendBitcoinSwapOut}} > 0) { + document.getElementById("swapAmount").value = {{.RecommendBitcoinSwapOut}}; + } } else { - document.getElementById("swapAmount").value = {{.RecommendLiquidSwapOut}}; + if ({{.RecommendLiquidSwapOut}} > 0) { + document.getElementById("swapAmount").value = {{.RecommendLiquidSwapOut}}; + } } } calculateTransactionFee(); @@ -176,6 +188,7 @@ let title = ""; let change = 25000; let asset = document.getElementById("from").value; + let discount = ""; if (asset == "ln") { asset = document.getElementById("to").value; @@ -306,10 +319,16 @@ }); // Calculate tx size assuming always two outputs - vbyteSize = 2503 + (inputs-1) * 84; + vbyteSize = 2420 + inputs * 84; + + {{if .HasDiscountedvSize}} + vbyteSize = 284 + inputs * 69; + discount =" discount"; + {{end}} + fee = Math.ceil(vbyteSize * feeRate); - // peerswap spends dust change as extra fee + // elements spends dust change as extra fee change = {{.LiquidBalance}} - swapAmount - fee; if (change < 1000) { @@ -324,13 +343,19 @@ vbyteSize = 350; // onchain.EstimatedOpeningTxSize title = "Assumed opening transaction size 350 vB"; } else { - vbyteSize = 3000; // prepaid - title = "Assumed opening transaction size 3000 vB"; + {{if .HasDiscountedvSize}} + vbyteSize = 750; // prepaid + title = "Assumed opening transaction size: 750 discounded vB"; + discount =" discount"; + {{else}} + vbyteSize = 3000; // prepaid + title = "Assumed opening transaction size: 3000 vB"; + {{end}} } fee = Math.ceil(vbyteSize * feeRate); } - - let text = "Transaction size: " + vbyteSize + " vBytes\n"; + + let text = "Transaction size: " + vbyteSize + discount + " vBytes\n"; text += "Estimated fee: " + formatWithThousandSeparators(fee) + " sats\n"; text += "Estimated PPM: " + formatWithThousandSeparators(Math.round(fee * 1000000 / swapAmount)); From 8767e53b8929d0bd96f9fb5f83ce67b6d30d1394 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Thu, 22 Aug 2024 16:07:54 +0200 Subject: [PATCH 38/98] Fix swap cost accounting --- CHANGELOG.md | 1 + cmd/psweb/bitcoin/bitcoin.go | 3 +- cmd/psweb/config/common.go | 1 + cmd/psweb/handlers.go | 47 +++++++++++------------ cmd/psweb/ln/cln.go | 2 +- cmd/psweb/ln/lnd.go | 2 +- cmd/psweb/main.go | 67 ++++++++++++++++++++------------- cmd/psweb/templates/peer.gohtml | 15 +++++--- 8 files changed, 78 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ed402..f77d455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Switch Custom Message serialization from JSON to GOB - Switch Custom Message type from 42067 to 42065 - Better estimate for swap-in fees and maximum swap amounts +- Fix accounting for initiated swap outs and failed swaps - {TODO] Account for circular rebalancing cost and PPM ## 1.6.8 diff --git a/cmd/psweb/bitcoin/bitcoin.go b/cmd/psweb/bitcoin/bitcoin.go index 7c1f070..5898342 100644 --- a/cmd/psweb/bitcoin/bitcoin.go +++ b/cmd/psweb/bitcoin/bitcoin.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "log" + "math" "net/http" "net/url" "time" @@ -231,7 +232,7 @@ func EstimateSatvB(targetConf uint) float64 { return 0 } - return feeInfo.Feerate * 100_000 + return math.Round(feeInfo.Feerate * 100_000) } func GetTxOutProof(txid string) (string, error) { diff --git a/cmd/psweb/config/common.go b/cmd/psweb/config/common.go index 64d0438..41c357f 100644 --- a/cmd/psweb/config/common.go +++ b/cmd/psweb/config/common.go @@ -94,6 +94,7 @@ func Load(dataDir string) { Config.NodeApi = "https://mempool.space/testnet/lightning/node" Config.BitcoinApi = "https://mempool.space/testnet" Config.LiquidApi = "https://liquid.network/testnet" + Config.ElementsPort = "7039" } // environment values take priority diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index 83883ff..813ff36 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -268,6 +268,7 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { swaps := res5.GetSwaps() senderInFee := int64(0) + senderOutFee := int64(0) receiverInFee := int64(0) receiverOutFee := int64(0) @@ -275,15 +276,23 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { switch swap.Type + swap.Role { case "swap-insender": if swap.PeerNodeId == id { - senderInFee += swapCost(swap) + cost, _ := swapCost(swap) + senderInFee += cost + } + case "swap-outsender": + if swap.PeerNodeId == id { + cost, _ := swapCost(swap) + senderOutFee += cost } case "swap-outreceiver": if swap.InitiatorNodeId == id { - receiverOutFee += swapCost(swap) + cost, _ := swapCost(swap) + receiverOutFee += cost } case "swap-inreceiver": if swap.InitiatorNodeId == id { - receiverInFee += swapCost(swap) + cost, _ := swapCost(swap) + receiverInFee += cost } } } @@ -315,7 +324,7 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { psPeer = false } else { if peer.AsSender.SatsOut > 0 { - senderOutFeePPM = int64(peer.PaidFee) * 1_000_000 / int64(peer.AsSender.SatsOut) + senderOutFeePPM = senderOutFee * 1_000_000 / int64(peer.AsSender.SatsOut) } if peer.AsSender.SatsIn > 0 { senderInFeePPM = senderInFee * 1_000_000 / int64(peer.AsSender.SatsIn) @@ -421,7 +430,7 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { swapFeeReserveBTC := uint64(math.Ceil(bitcoinFeeRate * 350)) // arbitrary haircut to avoid 'no matching outgoing channel available' - maxLiquidSwapIn := min(int64(satAmount)-int64(swapFeeReserveLBTC), int64(maxRemoteBalance)-10000) + maxLiquidSwapIn := min(int64(satAmount)-int64(swapFeeReserveLBTC()), int64(maxRemoteBalance)-10000) if maxLiquidSwapIn < 100_000 { maxLiquidSwapIn = 0 } @@ -431,7 +440,7 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { selectedChannel := peer.Channels[maxRemoteBalanceIndex].ChannelId if ptr := ln.LiquidBalances[peer.NodeId]; ptr != nil { peerLiquidBalance = int64(ptr.Amount) - maxLiquidSwapOut = uint64(max(0, min(int64(maxLocalBalance)-swapOutChannelReserve, peerLiquidBalance-swapOutChainReserve))) + maxLiquidSwapOut = uint64(max(0, min(int64(maxLocalBalance)-swapOutChannelReserve, peerLiquidBalance-int64(swapFeeReserveLBTC())))) } else { maxLiquidSwapOut = uint64(max(0, int64(maxLocalBalance)-swapOutChannelReserve)) @@ -447,7 +456,7 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { maxBitcoinSwapOut := uint64(0) if ptr := ln.BitcoinBalances[peer.NodeId]; ptr != nil { peerBitcoinBalance = int64(ptr.Amount) - maxBitcoinSwapOut = uint64(max(0, min(int64(maxLocalBalance)-swapOutChannelReserve, peerBitcoinBalance-swapOutChainReserve))) + maxBitcoinSwapOut = uint64(max(0, min(int64(maxLocalBalance)-swapOutChannelReserve, peerBitcoinBalance-int64(swapFeeReserveBTC)))) } else { maxBitcoinSwapOut = uint64(max(0, int64(maxLocalBalance)-swapOutChannelReserve)) } @@ -541,6 +550,7 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { ChannelInfo []*ln.ChanneInfo PeerSwapPeer bool MyAlias string + SenderOutFee int64 SenderOutFeePPM int64 SenderInFee int64 ReceiverInFee int64 @@ -590,6 +600,7 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { ChannelInfo: channelInfo, PeerSwapPeer: psPeer, MyAlias: ln.GetMyAlias(), + SenderOutFee: senderOutFee, SenderOutFeePPM: senderOutFeePPM, SenderInFee: senderInFee, ReceiverInFee: receiverInFee, @@ -600,7 +611,7 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { KeysendSats: keysendSats, OutputsBTC: &utxosBTC, OutputsLBTC: &utxosLBTC, - ReserveLBTC: swapFeeReserveLBTC, + ReserveLBTC: swapFeeReserveLBTC(), ReserveBTC: swapFeeReserveBTC, HasInboundFees: ln.HasInboundFees(), PeerBitcoinBalance: peerBitcoinBalance, @@ -1414,12 +1425,12 @@ func updateHandler(w http.ResponseWriter, r *http.Request) { swapData += `
LndChanId:` swapData += strconv.FormatUint(uint64(swap.LndChanId), 10) - cost := swapCost(swap) + cost, breakdown := swapCost(swap) if cost != 0 { ppm := cost * 1_000_000 / int64(swap.Amount) swapData += `
Swap Cost:` - swapData += formatSigned(cost) + " sats" + swapData += formatSigned(cost) + " sats (" + breakdown + ")" if swap.State == "State_ClaimedPreimage" { swapData += `
Cost PPM:` @@ -2341,22 +2352,6 @@ func submitHandler(w http.ResponseWriter, r *http.Request) { case "in": id, err = ps.SwapIn(client, swapAmount, channelId, asset, false) case "out": - peerId := peerNodeId[channelId] - var ptr *ln.BalanceInfo - - if asset == "btc" { - ptr = ln.BitcoinBalances[peerId] - } else if asset == "lbtc" { - ptr = ln.LiquidBalances[peerId] - } - - if ptr != nil { - if ptr.Amount < swapOutChainReserve || swapAmount > ptr.Amount-swapOutChainReserve { - redirectWithError(w, r, "/peer?id="+nodeId+"&", errors.New("swap amount exceeds peer's wallet balance less reserve "+formatWithThousandSeparators(swapOutChainReserve)+" sats")) - return - } - } - id, err = ps.SwapOut(client, swapAmount, channelId, asset, false) } diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index 4714425..e83452f 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -1043,7 +1043,7 @@ func EstimateFee() float64 { return 0 } - return float64(res.Details.Urgent) / 1000 + return math.Round(float64(res.Details.Urgent) / 1000) } // get fees for all channels by filling the maps [channelId] diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index d76c3ee..3317434 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -1774,7 +1774,7 @@ func EstimateFee() float64 { return 0 } - return float64(res.SatPerKw / 250) + return math.Round(float64(res.SatPerKw / 250)) } // get fees for all channels by filling the maps [channelId] diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index fd2e871..6084899 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "log" + "math" "net/http" "net/url" "os" @@ -39,10 +40,6 @@ const ( // Swap Out reserves are hardcoded here: // https://github.com/ElementsProject/peerswap/blob/c77a82913d7898d0d3b7c83e4a990abf54bd97e5/peerswaprpc/server.go#L105 swapOutChannelReserve = 5000 - // https://github.com/ElementsProject/peerswap/blob/c77a82913d7898d0d3b7c83e4a990abf54bd97e5/swap/actions.go#L388 - swapOutChainReserve = 20300 - // Swap In reserves - swapFeeReserveLBTC = 300 // Elements v23.2.2 introduced vsize discount elementsdFeeDiscountedVersion = 230202 ) @@ -362,7 +359,7 @@ func onTimer(firstRun bool) { go func() { r := internet.GetFeeRate() if r > 0 { - mempoolFeeRate = r + mempoolFeeRate = math.Round(r) } }() @@ -1027,11 +1024,11 @@ func convertSwapsToHTMLTable(swaps []*peerswaprpc.PrettyPrintSwap, nodeId string table += " ⚡ ⇨ " + asset } - cost := swapCost(swap) + cost, _ := swapCost(swap) if cost != 0 { totalCost += cost ppm := cost * 1_000_000 / int64(swap.Amount) - table += " " + formatSigned(cost) + "" + table += " " + formatSigned(cost) + "" } table += "" @@ -1297,8 +1294,7 @@ func cacheAliases() { // The goal is to spend maximum available liquid // To rebalance a channel with high enough historic fee PPM func findSwapInCandidate(candidate *SwapParams) error { - // extra 1000 to avoid no-change tx spending all on fees - minAmount := config.Config.AutoSwapThresholdAmount - swapFeeReserveLBTC + minAmount := config.Config.AutoSwapThresholdAmount - swapFeeReserveLBTC() minPPM := config.Config.AutoSwapThresholdPPM client, cleanup, err := ps.GetClient(config.Config.RpcHost) @@ -1494,7 +1490,7 @@ func executeAutoSwap() { return } - amount = min(amount, satAmount-swapFeeReserveLBTC) + amount = min(amount, satAmount-swapFeeReserveLBTC()) // execute swap id, err := ps.SwapIn(client, amount, candidate.ChannelId, "lbtc", false) @@ -1514,47 +1510,55 @@ func executeAutoSwap() { telegramSendMessage("🤖 Initiated Auto Swap-In with " + candidate.PeerAlias + " for " + formatWithThousandSeparators(amount) + " Liquid sats. Channel's PPM: " + formatWithThousandSeparators(candidate.PPM)) } -func swapCost(swap *peerswaprpc.PrettyPrintSwap) int64 { +// total and verbal breakdown +func swapCost(swap *peerswaprpc.PrettyPrintSwap) (int64, string) { if swap == nil { - return 0 - } - - if !stringIsInSlice(swap.State, []string{"State_ClaimedPreimage", "State_ClaimedCoop", "State_ClaimedCsv"}) { - return 0 + return 0, "" } fee := int64(0) + breakdown := "" switch swap.Type + swap.Role { case "swap-outsender": rebate, exists := ln.SwapRebates[swap.Id] if exists { + breakdown = fmt.Sprintf("rebate paid: %s", formatSigned(rebate)) fee = rebate } + claim := onchainTxFee(swap.Asset, swap.ClaimTxId) + fee += claim + breakdown += fmt.Sprintf(", claim: %s", formatSigned(claim)) case "swap-insender": fee = onchainTxFee(swap.Asset, swap.OpeningTxId) - if stringIsInSlice(swap.State, []string{"State_ClaimedCoop", "State_ClaimedCsv"}) { - // swap failed but we bear the claim cost - fee += onchainTxFee(swap.Asset, swap.ClaimTxId) + breakdown = fmt.Sprintf("opening: %s", formatSigned(fee)) + claim := onchainTxFee(swap.Asset, swap.ClaimTxId) + if claim > 0 { + fee += claim + breakdown = fmt.Sprintf(", claim: %s", formatSigned(claim)) } + case "swap-outreceiver": fee = onchainTxFee(swap.Asset, swap.OpeningTxId) + breakdown = fmt.Sprintf("opening: %s", formatSigned(fee)) rebate, exists := ln.SwapRebates[swap.Id] if exists { fee -= rebate - } - if stringIsInSlice(swap.State, []string{"State_ClaimedCoop", "State_ClaimedCsv"}) { - // swap failed but we bear the claim cost - fee += onchainTxFee(swap.Asset, swap.ClaimTxId) + breakdown += fmt.Sprintf(", rebate received: -%s", formatSigned(rebate)) } case "swap-inreceiver": fee = onchainTxFee(swap.Asset, swap.ClaimTxId) + breakdown = fmt.Sprintf("claim: %s", formatSigned(fee)) } - return fee + return fee, breakdown } // get tx fee from cache or online func onchainTxFee(asset, txId string) int64 { + if txId == "" { + return 0 + } + // try cache fee, exists := txFee[txId] if exists { @@ -1773,9 +1777,13 @@ func advertiseBalances() { } defer clean() - // store for replies to poll requests - ln.LiquidBalance = res2.GetSatAmount() ln.BitcoinBalance = uint64(ln.ConfirmedWalletBalance(cl)) + ln.LiquidBalance = res2.GetSatAmount() + + // Elements bug does not permit sending the whole balance, haircut it + if ln.LiquidBalance >= 2000 { + ln.LiquidBalance -= 2000 + } cutOff := time.Now().AddDate(0, 0, -1).Unix() - 120 @@ -1894,3 +1902,10 @@ func pollBalances() { log.Println("Polled peers for balances") } } + +func swapFeeReserveLBTC() uint64 { + if hasDiscountedvSize { + return 75 + } + return 300 +} diff --git a/cmd/psweb/templates/peer.gohtml b/cmd/psweb/templates/peer.gohtml index 6049014..8244181 100644 --- a/cmd/psweb/templates/peer.gohtml +++ b/cmd/psweb/templates/peer.gohtml @@ -331,10 +331,15 @@ // elements spends dust change as extra fee change = {{.LiquidBalance}} - swapAmount - fee; - if (change < 1000) { - // one output - vbyteSize -= 1,191; - // but the fee increases + // liquid dust size + if (change < 500) { + // one output only + {{if .HasDiscountedvSize}} + vbyteSize -= 109; + {{else}} + vbyteSize -= 1,191; + {{end}} + // but the fee increases by change fee += change; } } @@ -462,7 +467,7 @@ Sincerely, {{m .Peer.AsSender.SatsIn}} {{fs .SenderInFee}}{{if .SenderInFee}} ({{fs .SenderInFeePPM}}){{end}} {{m .Peer.AsSender.SatsOut}}{{fmt .Peer.PaidFee}}{{if .Peer.PaidFee}} ({{fs .SenderOutFeePPM}}){{end}}{{fs .SenderOutFee}}{{if .SenderOutFee}} ({{fs .SenderOutFeePPM}}){{end}}
Rcvd From dc1301a0aaee9f7d25ebfe6956f70ecdd5881bf3 Mon Sep 17 00:00:00 2001 From: Impa10r Date: Thu, 22 Aug 2024 21:08:01 +0200 Subject: [PATCH 39/98] Update changelog --- .vscode/launch.json | 2 +- CHANGELOG.md | 1 - cmd/psweb/templates/bitcoin.gohtml | 6 +++--- cmd/psweb/templates/peer.gohtml | 27 ++++++++++++++++++++++----- cmd/psweb/templates/reusable.gohtml | 2 +- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index eab2a29..2c86c18 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,7 @@ "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - //"args": ["-datadir", "/home/vlad/.peerswap_t4"] + "args": ["-datadir", "/home/vlad/.peerswap_t4"] //"args": ["-datadir", "/home/vlad/.peerswap2"] } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index f77d455..dbd13f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,6 @@ - Use exact fee rates for pegins and BTC withdrawals - Switch Custom Message serialization from JSON to GOB - Switch Custom Message type from 42067 to 42065 -- Better estimate for swap-in fees and maximum swap amounts - Fix accounting for initiated swap outs and failed swaps - {TODO] Account for circular rebalancing cost and PPM diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index 76d3b12..1389dab 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -88,7 +88,7 @@ {{end}}
- +
@@ -366,7 +366,7 @@ const payall = document.getElementById("subtractfee").checked; if (payall && fee >= amount / 4) { - alert("Total fee exceeding 25% of the amount is not allowed"); + alert("Total cost exceeding 25% of the amount is not allowed"); return false; } @@ -653,7 +653,7 @@ netAmount -= fee; } - text += "Total fee: " + formatWithThousandSeparators(fee) + " sats\n"; + text += "Total cost: " + formatWithThousandSeparators(fee) + " sats\n"; text += "Cost PPM: " + formatWithThousandSeparators(Math.round(fee * 1000000 / netAmount)); if (isPegin) { diff --git a/cmd/psweb/templates/peer.gohtml b/cmd/psweb/templates/peer.gohtml index 8244181..4165f12 100644 --- a/cmd/psweb/templates/peer.gohtml +++ b/cmd/psweb/templates/peer.gohtml @@ -89,7 +89,7 @@
- +
@@ -189,6 +189,8 @@ let change = 25000; let asset = document.getElementById("from").value; let discount = ""; + let claimText = ""; + let rebateText = ""; if (asset == "ln") { asset = document.getElementById("to").value; @@ -344,25 +346,40 @@ } } } else { + let claimSize = 149; // BTC if (asset == "btc") { vbyteSize = 350; // onchain.EstimatedOpeningTxSize title = "Assumed opening transaction size 350 vB"; } else { {{if .HasDiscountedvSize}} + claimSize = 330; vbyteSize = 750; // prepaid title = "Assumed opening transaction size: 750 discounded vB"; discount =" discount"; {{else}} + claimSize = 1350; vbyteSize = 3000; // prepaid title = "Assumed opening transaction size: 3000 vB"; {{end}} } - fee = Math.ceil(vbyteSize * feeRate); + + const rebate = Math.ceil(vbyteSize * feeRate); + const claim = Math.ceil(claimSize * feeRate); + fee = rebate + claim; + rebateText = "Rebate to opener: " + formatWithThousandSeparators(rebate) + " sats\n"; + claimText = "Claim tx fee: " + formatWithThousandSeparators(claim) + " sats\n"; } - let text = "Transaction size: " + vbyteSize + discount + " vBytes\n"; - text += "Estimated fee: " + formatWithThousandSeparators(fee) + " sats\n"; - text += "Estimated PPM: " + formatWithThousandSeparators(Math.round(fee * 1000000 / swapAmount)); + let text = "Opening tx size: " + vbyteSize + discount + " vBytes\n"; + text += "Opening tx fee: " + formatWithThousandSeparators(fee) + " sats\n"; + + if (rebateText != "") { + text = rebateText; + } + + text += claimText; + text += "Total cost: " + formatWithThousandSeparators(fee) + " sats\n"; + text += "Cost PPM: " + formatWithThousandSeparators(Math.round(fee * 1000000 / swapAmount)); if (asset == "btc" && change < 25000) { text += "\nWARNING: No reserve is left for anchor fee bumping!"; diff --git a/cmd/psweb/templates/reusable.gohtml b/cmd/psweb/templates/reusable.gohtml index fae9ef3..522ca12 100644 --- a/cmd/psweb/templates/reusable.gohtml +++ b/cmd/psweb/templates/reusable.gohtml @@ -128,7 +128,7 @@

- PeerSwap Web UI by Impa10r + PeerSwap Web UI by Impalor

" + sinceLastSwap := "" // Construct channels data for _, channel := range peer.Channels { @@ -562,6 +564,7 @@ func convertPeersToHTMLTable( if swapTimestamps[channel.ChannelId] > lastSwapTimestamp { lastSwapTimestamp = swapTimestamps[channel.ChannelId] tooltip = "Since the last swap " + timePassedAgo(time.Unix(lastSwapTimestamp, 0).UTC()) + sinceLastSwap = "since the last swap or " } stats := ln.GetChannelStats(channel.ChannelId, uint64(lastSwapTimestamp)) @@ -570,7 +573,8 @@ func convertPeersToHTMLTable( inflows := stats.RoutedIn + stats.InvoicedIn totalForwardsOut += stats.RoutedOut totalForwardsIn += stats.RoutedIn - totalCost += stats.PaidCost + rebalanceCost += stats.RebalanceCost + rebalanceAmount += stats.RebalanceIn totalPayments += stats.PaidOut netFlow := float64(int64(inflows) - int64(outflows)) @@ -684,17 +688,17 @@ func convertPeersToHTMLTable( if totalForwardsOut > 0 { ppmRevenue = totalFees * 1_000_000 / totalForwardsOut } - if totalPayments > 0 { - ppmCost = totalCost * 1_000_000 / totalPayments + if rebalanceAmount > 0 { + ppmCost = rebalanceCost * 1_000_000 / rebalanceAmount } - peerTable += "" + formatWithThousandSeparators(totalFees) + "" - if totalCost > 0 { + peerTable += "" + formatWithThousandSeparators(totalFees) + "" + if rebalanceCost > 0 { color := "red" if config.Config.ColorScheme == "dark" { color = "pink" } - peerTable += " -" + formatWithThousandSeparators(totalCost) + "" + peerTable += " -" + formatWithThousandSeparators(rebalanceCost) + "" } peerTable += "
" @@ -775,7 +779,8 @@ func convertOtherPeersToHTMLTable(peers []*peerswaprpc.PeerSwapPeer, var totalForwardsIn uint64 var totalPayments uint64 var totalFees uint64 - var totalCost uint64 + var rebalanceCost uint64 + var rebalanceAmount uint64 channelsTable := "" @@ -819,7 +824,8 @@ func convertOtherPeersToHTMLTable(peers []*peerswaprpc.PeerSwapPeer, totalForwardsOut += stats.RoutedOut totalForwardsIn += stats.RoutedIn totalPayments += stats.PaidOut - totalCost += stats.PaidCost + rebalanceCost += stats.RebalanceCost + rebalanceAmount += stats.RebalanceIn netFlow := float64(int64(inflows) - int64(outflows)) @@ -921,15 +927,15 @@ func convertOtherPeersToHTMLTable(peers []*peerswaprpc.PeerSwapPeer, ppmRevenue = totalFees * 1_000_000 / totalForwardsOut } peerTable += "" + formatWithThousandSeparators(totalFees) + " " - if totalPayments > 0 { - ppmCost = totalCost * 1_000_000 / totalPayments + if rebalanceAmount > 0 { + ppmCost = rebalanceCost * 1_000_000 / rebalanceAmount } - if totalCost > 0 { + if rebalanceCost > 0 { color := "red" if config.Config.ColorScheme == "dark" { color = "pink" } - peerTable += " -" + formatWithThousandSeparators(totalCost) + "" + peerTable += " -" + formatWithThousandSeparators(rebalanceCost) + "" } peerTable += "
" From 718a7237695b6afec113ff74244bd63db162988c Mon Sep 17 00:00:00 2001 From: Impa10r Date: Sat, 24 Aug 2024 18:47:30 +0200 Subject: [PATCH 41/98] Allow sole CT claims if noone joins --- .vscode/launch.json | 4 ++-- CHANGELOG.md | 10 +++++++--- README.md | 8 ++++---- cmd/psweb/bitcoin/bitcoin.go | 23 +++++++++++++++++++++++ cmd/psweb/handlers.go | 17 +++++++++++++---- cmd/psweb/ln/claimjoin.go | 7 +++++-- cmd/psweb/main.go | 15 +++++---------- cmd/psweb/templates/bitcoin.gohtml | 12 ++++++++---- cmd/psweb/templates/homepage.gohtml | 2 +- cmd/psweb/templates/loading.gohtml | 6 ++---- cmd/psweb/templates/peer.gohtml | 6 +++--- 11 files changed, 73 insertions(+), 37 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2451d17..efce92a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,8 +13,8 @@ "program": "${workspaceFolder}/cmd/psweb/", "showLog": false, "envFile": "${workspaceFolder}/.env", - //"args": ["-datadir", "/home/vlad/.peerswap_t4"] - "args": ["-datadir", "/home/vlad/.peerswap3"] + "args": ["-datadir", "/home/vlad/.peerswap_t4"] + //"args": ["-datadir", "/home/vlad/.peerswap3"] } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index e333e17..0b37159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,17 @@ ## 1.7.0 -- Correct swap fee estimates for discounted vsize (Elements v23.2.2+) - LND: Implement confidential joint pegin claims (Elements v23.2.2+) -- Use exact fee rates for pegins and BTC withdrawals +- Correct swap fee estimates for discounted vsize (Elements v23.2.2+) +- Achieve exact fee rates for pegins and BTC withdrawals - Switch Custom Message serialization from JSON to GOB - Switch Custom Message type from 42067 to 42065 - Fix accounting for initiated swap outs and failed swaps -- Show circular rebalancing cost and PPM instead of paid fees +- LND: Show circular rebalancing cost and PPM instead of paid fees +- Update keysend invite message with mention of discount CT + +## 1.6.9 +- Hot Fix bitcoinswaps=true persisting in peerswap.conf on psweb restart ## 1.6.8 diff --git a/README.md b/README.md index 93dd365..ce58ad4 100644 --- a/README.md +++ b/README.md @@ -217,13 +217,13 @@ alias ecli="docker exec -it elements_node_1 elements-cli -rpcuser=elements -rpcp ## Confidential Liquid Pegin -Elements Core v23.2.2 introduced vsize discount for confidential transactions. Now sending a Liquid payment with a blinded amount costs the same or cheaper than a publicly visible (explicit) one. For example, claiming a pegin with ```elements-cli claimpegin``` costs about 45 sats, but it is possible to manually construct the same transaction (```elements-cli createrawtransaction```) with confidential destination address, blind and sign it, then post and only pay 36 sats. However, from privacy perspective, blinding a single pegin claim makes little sense. The linked Bitcoin UTXO will still show the explicit amount, so it is easily traceable to your new Liquid address. To achieve a truly confidential pegin, it is necessary to mix two or more independent claims into one single transaction, a-la CoinJoin. +Elements Core v23.2.2 introduced vsize discount for confidential transactions. Now sending a Liquid payment with a blinded amount costs the same or cheaper than a publicly visible (explicit) one. For example, claiming a pegin with ```elements-cli claimpegin``` costs about 45 sats, but it is possible to manually construct the same transaction (```elements-cli createrawtransaction```) with confidential destination address, blind and sign it, then post and pay a lower fee. However, from privacy perspective, blinding a single pegin claim makes little sense. The linked Bitcoin UTXO will still show the explicit amount, so it is easily traceable to your new Liquid address. To achieve a truly confidential pegin, it is necessary to mix two or more independent claims into one single transaction, a-la CoinJoin. -In v1.7.0 PSWeb implemented such "ClaimJoin". If you opt in when starting your pegin, your node will send invitations to all other PSWeb nodes to join in while you wait for your 102 confirmations. To join your claim, they should opt in as well while starting their own pegins. They won't know which node initiated the ClaimJoin and who else will be joining. The initiator also won't know which nodes responded. All communication happens blindly via single use public/private key pairs (secp256k1). Nodes who do not directly participate act as relays for the encrypted messages, not being able to read them and not knowing the source and the final destination. This way our ClaimJoin coordination is fully confidential and not limited to direct peers. +In v1.7.0 PSWeb implemented such "ClaimJoin". If you opt in when starting your pegin, your node will send invitations to all other PSWeb nodes to join in while you wait for your 102 confirmations. To join your claim, they should opt in while starting their own pegins. Peers won't know which node initiated the ClaimJoin and who else will be joining. The initiator also won't know which nodes responded. All communication happens blindly via single use public/private key pairs (secp256k1). Nodes who do not directly participate act as relays for the encrypted messages, not being able to read them and not knowing the sources and the final destinations. This way our ClaimJoin coordination is fully confidential and not limited to direct peers. A node responds to an invitation by anonymously sending details of its pegin funding transaction to the initiator. -When all N pegins mature, the initiator node prepares one large PSET with N pegin inputs and N CT outputs, shuffled randomly, and sends it secuentially to all participants: first to blind Liquid outputs and then sign pegin inputs. Before blinding/signing and returning the PSET, each joiner verifies that his output address is there for the correct amount (allowing for upto 50 sats fee haircut). Upto 10 claims can be joined this way, to fit into one custom message (64kb). The price for additional privacy is time. For the initiator the wait can take upto 34 hours if the finel peers joins at block 101. While for that last joiner the wait will be the same 17 hours as a single pegin. In practice, the blinding and signing round needs to be done twice: first to find out the exact discount vsize of the final transaction, then to pay the correct fee at 0.1 sat/vb. If the fee cannot be divided equally, the last participant pays slightnly more as an incentive to join early. +When all N pegins mature, the initiator node prepares one large PSET with N pegin inputs and N CT outputs, shuffled randomly, and sends it secuentially to all participants: first to blind Liquid outputs and then to sign pegin inputs. Before blinding/signing and returning the PSET, each joiner verifies that his output address is there for the correct amount (allowing for small fee haircut). Upto 10 claims can be joined this way, to fit into one custom message (64kb). The price for additional privacy is time. For the initiator the wait can take upto 34 hours if the final peer joins at block 101. For that last joiner the wait will be the same 17 hours as in a single pegin. In practice, the blinding and signing round may need to be done twice: first to find out the exact discounted vsize of the final transaction, then to set the correct fee at 0.1 sat/vb. If the fee cannot be divided equally, the last joiner pays slightnly more as an incentive to join earlier next time. -The process bears no risk to the participants. If any joiner becomes unresponsive during the blinding/signing round, it is automatically kicked out. If the initiator fails to complete the process, each joiner reverts to single pegin claim 10 blocks after the final maturity. As the last resort, you can always [claim your pegin manually](#liquid-pegin) with ```elements-cli```. All the necessary details are in PSWeb log. Your claim script and pegin transaction can only be used by your own Liquid wallet (private key), so there is no risk to share them. +The process bears no risk to the participants. If any joiner becomes unresponsive during the blinding/signing round, he is automatically kicked out. If the initiator fails to complete the process, each joiner reverts to single pegin claim 10 blocks after the final maturity. As the last resort, if PSWeb dies completely, you can always [claim your pegin manually](#liquid-pegin) with ```elements-cli```. All the necessary details will be in PSWeb log. Your claim script and pegin transaction can only be used by your own Liquid wallet's private key, so there is no risk to share them. Blinding and signing your part happens locally on your node, no sensitive info is transmitted outside. # Support diff --git a/cmd/psweb/bitcoin/bitcoin.go b/cmd/psweb/bitcoin/bitcoin.go index 5898342..938b3f3 100644 --- a/cmd/psweb/bitcoin/bitcoin.go +++ b/cmd/psweb/bitcoin/bitcoin.go @@ -235,6 +235,29 @@ func EstimateSatvB(targetConf uint) float64 { return math.Round(feeInfo.Feerate * 100_000) } +// returns block hash +func GetBlockHash(block uint32) (string, error) { + client := BitcoinClient() + service := &Bitcoin{client} + params := &[]interface{}{block} + + r, err := service.client.call("getblockhash", params, "") + if err = handleError(err, &r); err != nil { + log.Printf("GetBlockHash: %v", err) + return "", err + } + + var response string + + err = json.Unmarshal([]byte(r.Result), &response) + if err != nil { + log.Printf("GetBlockHash unmarshall: %v", err) + return "", err + } + + return response, nil +} + func GetTxOutProof(txid string) (string, error) { client := BitcoinClient() service := &Bitcoin{client} diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index c5ee918..8521d2b 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -864,10 +864,19 @@ func peginHandler(w http.ResponseWriter, r *http.Request) { // test on a pre-existing tx that bitcon core can complete the peg tx := "b61ec844027ce18fd3eb91fa7bed8abaa6809c4d3f6cf4952b8ebaa7cd46583a" if config.Config.Chain == "testnet" { - if hasDiscountedvSize { - tx = "0b387c3b7a8d9fad4d7a1ac2cba4958451c03d6c4fe63dfbe10cdb86d666cdd7" - } else { + // identify testnet blockchain + genesisHash, err := bitcoin.GetBlockHash(0) + if err != nil { + redirectWithError(w, r, "/bitcoin?", err) + return + } + + if genesisHash == "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943" { + // testnet3 tx = "2c7ec5043fe8ee3cb4ce623212c0e52087d3151c9e882a04073cce1688d6fc1e" + } else { + // testnet4 + tx = "0b387c3b7a8d9fad4d7a1ac2cba4958451c03d6c4fe63dfbe10cdb86d666cdd7" } } @@ -924,7 +933,7 @@ func peginHandler(w http.ResponseWriter, r *http.Request) { if hasDiscountedvSize && ln.Implementation == "LND" { config.Config.PeginClaimJoin = r.FormValue("claimJoin") == "on" if config.Config.PeginClaimJoin { - ln.ClaimStatus = "Awaiting tx confirmation" + ln.ClaimStatus = "Awaiting funding tx to confirm" db.Save("ClaimJoin", "ClaimStatus", ln.ClaimStatus) } } else { diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 2c06588..26a406b 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -134,13 +134,12 @@ func loadClaimJoinDB() { // runs every block func OnBlock(blockHeight uint32) { - if !config.Config.PeginClaimJoin || MyRole != "initiator" || len(ClaimParties) < 2 || blockHeight < ClaimBlockHeight { + if !config.Config.PeginClaimJoin || MyRole != "initiator" || blockHeight < ClaimBlockHeight { return } // initial fee estimate totalFee := 41 + 30*(len(ClaimParties)-1) - errorCounter := 0 create_pset: @@ -148,6 +147,8 @@ create_pset: var err error claimPSET, err = createClaimPSET(totalFee) if err != nil { + // claimjoin failed + EndClaimJoin("", err.Error()) return } db.Save("ClaimJoin", "claimPSET", &claimPSET) @@ -1002,6 +1003,8 @@ func EndClaimJoin(txId string, status string) bool { // signal to telegram bot config.Config.PeginTxId = txId config.Config.PeginClaimScript = "done" + } else { + log.Println("ClaimJoin pegin failed") } resetClaimJoin() return true diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 6c04f9b..83ef348 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -524,7 +524,7 @@ func convertPeersToHTMLTable( var rebalanceAmount uint64 channelsTable := "" - sinceLastSwap := "" + sinceLastSwap := "for the previous 6 months" // Construct channels data for _, channel := range peer.Channels { @@ -564,7 +564,7 @@ func convertPeersToHTMLTable( if swapTimestamps[channel.ChannelId] > lastSwapTimestamp { lastSwapTimestamp = swapTimestamps[channel.ChannelId] tooltip = "Since the last swap " + timePassedAgo(time.Unix(lastSwapTimestamp, 0).UTC()) - sinceLastSwap = "since the last swap or " + sinceLastSwap = "since the last swap" } stats := ln.GetChannelStats(channel.ChannelId, uint64(lastSwapTimestamp)) @@ -692,13 +692,13 @@ func convertPeersToHTMLTable( ppmCost = rebalanceCost * 1_000_000 / rebalanceAmount } - peerTable += "" + formatWithThousandSeparators(totalFees) + "" + peerTable += "" + formatWithThousandSeparators(totalFees) + "" if rebalanceCost > 0 { color := "red" if config.Config.ColorScheme == "dark" { color = "pink" } - peerTable += " -" + formatWithThousandSeparators(rebalanceCost) + "" + peerTable += " -" + formatWithThousandSeparators(rebalanceCost) + "" } peerTable += "
" @@ -1143,12 +1143,7 @@ func checkPegin() { if ln.MyRole != "none" { // 10 blocks to wait before switching back to individual claim - margin := uint32(10) - if ln.MyRole == "initiator" && len(ln.ClaimParties) < 2 { - // if no one has joined, switch on mturity - margin = 0 - } - if currentBlockHeight >= ln.ClaimBlockHeight+margin { + if currentBlockHeight >= ln.ClaimBlockHeight+10 { // claim pegin individually t := "ClaimJoin expired, falling back to the individual claim" log.Println(t) diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index 1389dab..337044d 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -365,9 +365,13 @@ const amount = Number(document.getElementById("peginAmount").value); const payall = document.getElementById("subtractfee").checked; - if (payall && fee >= amount / 4) { - alert("Total cost exceeding 25% of the amount is not allowed"); - return false; + if (fee > amount / 1000) { + // Display confirmation dialog + var confirmed = confirm("Cost exceeds 1000 PPM. Are you sure?"); + if (!confirmed) { + // user cancels, prevent form submission + return false; + } } {{if not .IsCLN}} @@ -639,7 +643,7 @@ fee += 45; {{if .CanClaimJoin}} if (document.getElementById("claimJoin").checked) { - liquidFee = "35-45" + liquidFee = "30-45" } {{end}} text += "Liquid chain fee: " + liquidFee + " sats\n"; diff --git a/cmd/psweb/templates/homepage.gohtml b/cmd/psweb/templates/homepage.gohtml index 30262af..05b7b20 100644 --- a/cmd/psweb/templates/homepage.gohtml +++ b/cmd/psweb/templates/homepage.gohtml @@ -56,7 +56,7 @@ {{.ListPeers}} {{if eq .OtherPeers ""}}
- +
{{end}} diff --git a/cmd/psweb/templates/loading.gohtml b/cmd/psweb/templates/loading.gohtml index bebafb0..9744caf 100644 --- a/cmd/psweb/templates/loading.gohtml +++ b/cmd/psweb/templates/loading.gohtml @@ -21,16 +21,14 @@ const timer = setInterval(() => { if (progressValue < 100) { - progressValue += 1; + progressValue += 0.01; progressBar.style.width = `${progressValue}%`; } if (progressValue === 100) { clearInterval(timer); - // Redirect to home page - // window.location.href = '/'; } - }, 600); // 60 seconds wait + }, 6); // 60 seconds wait
diff --git a/cmd/psweb/templates/peer.gohtml b/cmd/psweb/templates/peer.gohtml index 4165f12..c31a08d 100644 --- a/cmd/psweb/templates/peer.gohtml +++ b/cmd/psweb/templates/peer.gohtml @@ -413,11 +413,11 @@