From 5a509248164bbea0edb79b8ede60bf723730d1f7 Mon Sep 17 00:00:00 2001 From: WeebDataHoarder <57538841+WeebDataHoarder@users.noreply.github.com> Date: Sun, 7 Apr 2024 19:29:22 +0200 Subject: [PATCH] Remove git.gammaspectra.live/P2Pool/go-monero dependency, replace with pkg/rpc and pkg/levin inline --- go.mod | 9 +- go.sum | 19 +- monero/client/client.go | 4 +- monero/client/levin/LICENSE | 201 ++++ monero/client/levin/README.md | 3 + monero/client/levin/boost.go | 74 ++ monero/client/levin/client.go | 144 +++ monero/client/levin/levin.go | 307 ++++++ monero/client/levin/levin_test.go | 151 +++ monero/client/levin/node.go | 133 +++ monero/client/levin/portable_storage.go | 410 ++++++++ monero/client/levin/portable_storage_test.go | 225 +++++ monero/client/rpc/LICENSE | 201 ++++ monero/client/rpc/README.md | 3 + monero/client/rpc/client.go | 223 +++++ monero/client/rpc/client_export_test.go | 3 + monero/client/rpc/client_test.go | 159 +++ monero/client/rpc/daemon/binary_endpoints.go | 69 ++ monero/client/rpc/daemon/client.go | 51 + .../client/rpc/daemon/daemon_example_test.go | 32 + monero/client/rpc/daemon/doc.go | 4 + monero/client/rpc/daemon/jsonrpc.go | 432 ++++++++ monero/client/rpc/daemon/raw_endpoints.go | 259 +++++ monero/client/rpc/daemon/types.go | 941 ++++++++++++++++++ monero/client/rpc/doc.go | 4 + monero/client/tx.go | 2 +- 26 files changed, 4057 insertions(+), 6 deletions(-) create mode 100644 monero/client/levin/LICENSE create mode 100644 monero/client/levin/README.md create mode 100644 monero/client/levin/boost.go create mode 100644 monero/client/levin/client.go create mode 100644 monero/client/levin/levin.go create mode 100644 monero/client/levin/levin_test.go create mode 100644 monero/client/levin/node.go create mode 100644 monero/client/levin/portable_storage.go create mode 100644 monero/client/levin/portable_storage_test.go create mode 100644 monero/client/rpc/LICENSE create mode 100644 monero/client/rpc/README.md create mode 100644 monero/client/rpc/client.go create mode 100644 monero/client/rpc/client_export_test.go create mode 100644 monero/client/rpc/client_test.go create mode 100644 monero/client/rpc/daemon/binary_endpoints.go create mode 100644 monero/client/rpc/daemon/client.go create mode 100644 monero/client/rpc/daemon/daemon_example_test.go create mode 100644 monero/client/rpc/daemon/doc.go create mode 100644 monero/client/rpc/daemon/jsonrpc.go create mode 100644 monero/client/rpc/daemon/raw_endpoints.go create mode 100644 monero/client/rpc/daemon/types.go create mode 100644 monero/client/rpc/doc.go diff --git a/go.mod b/go.mod index 9e0d8a7..3ab2096 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.22 require ( git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20240405085108-e2f706cb5c00 - git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 git.gammaspectra.live/P2Pool/monero-base58 v1.0.0 git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 @@ -13,6 +12,8 @@ require ( github.com/floatdrop/lru v1.3.0 github.com/go-zeromq/zmq4 v0.16.1-0.20240124085909-e75c615ba1b3 github.com/goccy/go-json v0.10.2 + github.com/sclevine/spec v1.4.0 + github.com/stretchr/testify v1.8.1 github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc golang.org/x/sys v0.19.0 lukechampine.com/uint128 v1.3.0 @@ -20,10 +21,16 @@ require ( require ( github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dolthub/maphash v0.1.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.22.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/text v0.14.0 // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 diff --git a/go.sum b/go.sum index 6cdf1d5..8e2b946 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20240405085108-e2f706cb5c00 h1:mDQY337iKB+kle5RYWL5CoAz+3DmnkAh/B2XD8B+PFk= git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20240405085108-e2f706cb5c00/go.mod h1:FZsrMWGucMP3SZamzrd7m562geIs5zp1O/9MGoiAKH0= -git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE= -git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g= git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8= git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c= git.gammaspectra.live/P2Pool/monero-base58 v1.0.0 h1:s8LZxVNc93YEs2NCCNWZ7CKr8RbEb031y6Wkvhn+TS4= @@ -16,6 +14,8 @@ github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= @@ -24,10 +24,21 @@ github.com/dolthub/swiss v0.2.2-0.20240312182618-f4b2babd2bc1 h1:F7u1ZVCidajlPuJ github.com/dolthub/swiss v0.2.2-0.20240312182618-f4b2babd2bc1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0= github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c= github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= @@ -40,6 +51,10 @@ golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= diff --git a/monero/client/client.go b/monero/client/client.go index a80ec5f..b9a2054 100644 --- a/monero/client/client.go +++ b/monero/client/client.go @@ -4,11 +4,11 @@ import ( "context" "errors" "fmt" + "git.gammaspectra.live/P2Pool/consensus/v3/monero/client/rpc" + "git.gammaspectra.live/P2Pool/consensus/v3/monero/client/rpc/daemon" "git.gammaspectra.live/P2Pool/consensus/v3/monero/transaction" "git.gammaspectra.live/P2Pool/consensus/v3/types" "git.gammaspectra.live/P2Pool/consensus/v3/utils" - "git.gammaspectra.live/P2Pool/go-monero/pkg/rpc" - "git.gammaspectra.live/P2Pool/go-monero/pkg/rpc/daemon" "github.com/floatdrop/lru" fasthex "github.com/tmthrgd/go-hex" "sync" diff --git a/monero/client/levin/LICENSE b/monero/client/levin/LICENSE new file mode 100644 index 0000000..ffdef91 --- /dev/null +++ b/monero/client/levin/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 Ciro S. Costa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/monero/client/levin/README.md b/monero/client/levin/README.md new file mode 100644 index 0000000..52b454d --- /dev/null +++ b/monero/client/levin/README.md @@ -0,0 +1,3 @@ +# go-monero RPC + +Taken from [git.gammaspectra.live/P2Pool/go-monero](https://git.gammaspectra.live/P2Pool/go-monero/src/commit/910450c4a523a8a21c0bcabf7d303418e6c76d50/pkg/levin) \ No newline at end of file diff --git a/monero/client/levin/boost.go b/monero/client/levin/boost.go new file mode 100644 index 0000000..30da968 --- /dev/null +++ b/monero/client/levin/boost.go @@ -0,0 +1,74 @@ +package levin + +import ( + "encoding/binary" + "fmt" +) + +const ( + BoostSerializeTypeInt64 byte = 0x1 + BoostSerializeTypeInt32 byte = 0x2 + BoostSerializeTypeInt16 byte = 0x3 + BoostSerializeTypeInt8 byte = 0x4 + + BoostSerializeTypeUint64 byte = 0x5 + BoostSerializeTypeUint32 byte = 0x6 + BoostSerializeTypeUint16 byte = 0x7 + BoostSerializeTypeUint8 byte = 0x8 + + BoostSerializeTypeDouble byte = 0x9 + + BoostSerializeTypeString byte = 0x0a + BoostSerializeTypeBool byte = 0x0b + BoostSerializeTypeObject byte = 0x0c + BoostSerializeTypeArray byte = 0xd + + BoostSerializeFlagArray byte = 0x80 +) + +type BoostByte byte + +func (v BoostByte) Bytes() []byte { + return []byte{ + BoostSerializeTypeUint8, + byte(v), + } +} + +type BoostUint32 uint32 + +func (v BoostUint32) Bytes() []byte { + b := []byte{ + BoostSerializeTypeUint32, + 0x00, 0x00, 0x00, 0x00, + } + binary.LittleEndian.PutUint32(b[1:], uint32(v)) + return b +} + +type BoostUint64 uint64 + +func (v BoostUint64) Bytes() []byte { + b := []byte{ + BoostSerializeTypeUint64, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + + binary.LittleEndian.PutUint64(b[1:], uint64(v)) + + return b +} + +type BoostString string + +func (v BoostString) Bytes() []byte { + b := []byte{BoostSerializeTypeString} + + varInB, err := VarIn(len(v)) + if err != nil { + panic(fmt.Errorf("varin '%d': %w", len(v), err)) + } + + return append(b, append(varInB, []byte(v)...)...) +} diff --git a/monero/client/levin/client.go b/monero/client/levin/client.go new file mode 100644 index 0000000..e893972 --- /dev/null +++ b/monero/client/levin/client.go @@ -0,0 +1,144 @@ +package levin + +import ( + "bytes" + "context" + "fmt" + "io" + "net" + "time" +) + +const DialTimeout = 15 * time.Second + +type Client struct { + conn net.Conn +} + +type ClientConfig struct { + ContextDialer ContextDialer +} + +type ClientOption func(*ClientConfig) + +type ContextDialer interface { + DialContext(ctx context.Context, network, addr string) (net.Conn, error) +} + +func WithContextDialer(v ContextDialer) func(*ClientConfig) { + return func(c *ClientConfig) { + c.ContextDialer = v + } +} + +func NewClient(ctx context.Context, addr string, opts ...ClientOption) (*Client, error) { + cfg := &ClientConfig{ + ContextDialer: &net.Dialer{}, + } + for _, opt := range opts { + opt(cfg) + } + + conn, err := cfg.ContextDialer.DialContext(ctx, "tcp", addr) + if err != nil { + return nil, fmt.Errorf("dial ctx: %w", err) + } + + return &Client{ + conn: conn, + }, nil +} + +func (c *Client) Close() error { + if c.conn == nil { + return nil + } + + if err := c.conn.Close(); err != nil { + return fmt.Errorf("close: %w", err) + } + + return nil +} + +func (c *Client) Handshake(ctx context.Context) (*Node, error) { + payload := (&PortableStorage{ + Entries: []Entry{ + { + Name: "node_data", + Serializable: &Section{ + Entries: []Entry{ + { + Name: "network_id", + Serializable: BoostString(string(MainnetNetworkId)), + }, + }, + }, + }, + }, + }).Bytes() + + reqHeaderB := NewRequestHeader(CommandHandshake, uint64(len(payload))).Bytes() + + if _, err := c.conn.Write(reqHeaderB); err != nil { + return nil, fmt.Errorf("write header: %w", err) + } + + if _, err := c.conn.Write(payload); err != nil { + return nil, fmt.Errorf("write payload: %w", err) + } + +again: + responseHeaderB := make([]byte, LevinHeaderSizeBytes) + if _, err := io.ReadFull(c.conn, responseHeaderB); err != nil { + return nil, fmt.Errorf("read full header: %w", err) + } + + respHeader, err := NewHeaderFromBytesBytes(responseHeaderB) + if err != nil { + return nil, fmt.Errorf("new header from resp bytes: %w", err) + } + + dest := new(bytes.Buffer) + + if respHeader.Length != 0 { + if _, err := io.CopyN(dest, c.conn, int64(respHeader.Length)); err != nil { + return nil, fmt.Errorf("copy payload to stdout: %w", err) + } + } + + if respHeader.Command != CommandHandshake { + dest.Reset() + goto again + } + + ps, err := NewPortableStorageFromBytes(dest.Bytes()) + if err != nil { + return nil, fmt.Errorf("new portable storage from bytes: %w", err) + } + + peerList := NewNodeFromEntries(ps.Entries) + return &peerList, nil +} + +func (c *Client) Ping(ctx context.Context) error { + reqHeaderB := NewRequestHeader(CommandPing, 0).Bytes() + + if _, err := c.conn.Write(reqHeaderB); err != nil { + return fmt.Errorf("write: %w", err) + } + + responseHeaderB := make([]byte, LevinHeaderSizeBytes) + if _, err := io.ReadFull(c.conn, responseHeaderB); err != nil { + return fmt.Errorf("read full header: %w", err) + } + + respHeader, err := NewHeaderFromBytesBytes(responseHeaderB) + if err != nil { + return fmt.Errorf("new header from resp bytes: %w", err) + } + + fmt.Printf("%+v\n", respHeader) + + return nil +} diff --git a/monero/client/levin/levin.go b/monero/client/levin/levin.go new file mode 100644 index 0000000..45bb60a --- /dev/null +++ b/monero/client/levin/levin.go @@ -0,0 +1,307 @@ +// +// see https://github.com/monero-project/monero/blob/e45619e61e4831eea70a43fe6985f4d57ea02e9e/contrib/epee/include/net/levin_base.h +// see https://github.com/monero-project/monero/blob/e45619e61e4831eea70a43fe6985f4d57ea02e9e/docs/LEVIN_PROTOCOL.md + +package levin + +import ( + "encoding/binary" + "fmt" +) + +const ( + LevinSignature uint64 = 0x0101010101012101 // Dander's Nightmare + + LevinProtocolVersion uint32 = 1 + + LevinPacketRequest uint32 = 0x00000001 // Q flag + LevinPacketReponse uint32 = 0x00000002 // S flag + LevinPacketMaxDefaultSize uint64 = 100000000 // 100MB _after_ handshake + LevinPacketMaxInitialSize uint64 = 256 * 1024 // 256KiB _before_ handshake + + LevinHeaderSizeBytes = 33 +) + +const ( + // Return Codes. + LevinOk int32 = 0 + LevinErrorConnection int32 = -1 + LevinErrorConnectionNotFound int32 = -2 + LevinErrorConnectionDestroyed int32 = -3 + LevinErrorConnectionTimedout int32 = -4 + LevinErrorConnectionNoDuplexProtocol int32 = -5 + LevinErrorConnectionHandlerNotDefined int32 = -6 + LevinErrorFormat int32 = -7 +) + +func IsValidReturnCode(c int32) bool { + // anything >= 0 is good (there are some `1`s in the code :shrug:) + return c >= LevinErrorFormat +} + +const ( + // p2p admin commands. + CommandHandshake uint32 = 1001 + CommandTimedSync uint32 = 1002 + CommandPing uint32 = 1003 + CommandStat uint32 = 1004 + CommandNetworkState uint32 = 1005 + CommandPeerID uint32 = 1006 + CommandSupportFlags uint32 = 1007 +) + +var ( + MainnetNetworkId = []byte{ + 0x12, 0x30, 0xf1, 0x71, + 0x61, 0x04, 0x41, 0x61, + 0x17, 0x31, 0x00, 0x82, + 0x16, 0xa1, 0xa1, 0x10, + } + + MainnetGenesisTx = "418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3" +) + +func IsValidCommand(c uint32) bool { + return (c >= CommandHandshake && c <= CommandSupportFlags) +} + +// +// Header +// +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | 0x01 | 0x21 | 0x01 | 0x01 | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | 0x01 | 0x01 | 0x01 | 0x01 | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Length | +// | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | E. Response | _ Command _ +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | _ Return Code _ +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |Q|S|B|E| _ Reserved_ +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | 0x01 | 0x00 | 0x00 | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | 0x00 | +// +-+-+-+-+-+-+-+-+ +// +// +// i.e., +// +// BYTE(0X01) BYTE(0X21) BYTE(0X01) BYTE(0X01) ---. +// +--> protocol identification +// BYTE(0X01) BYTE(0X01) BYTE(0X01) BYTE(0X01) ---' +// +// +// UINT64(LENGTH) -----------------------------------> unsigned little-endian 64bit integer +// length of the payload _not including_ +// the header. messages >100MB are rejected. +// +// +// BYTE(E.RESPONSE) 4BYTE(COMMAND) 4BYTE(RET CODE) +// | | | +// | | | +// | | '-> signed 32-bit little endian integer representing the response +// | | from the peer from the last command invoked. `0` for request msgs. +// | | +// | '-> unsigned 32-bit little endian integer +// | representing the monero specific cmd +// | +// '-> zero-byte if no response is expected from the peer, non-zero if response is expected. +// peers must respond to requests w/ this flag in the same order as received. +// +// +// BIT(Q) BIT(S) BIT(B) BIT(E) 3BYTE+4BIT(RESERVED) +// | | | | +// | | | | +// | | | '-> set if this is the end of a frag msg +// | | | +// | | '-> set if this is the beginning of a frag msg +// | | +// | '-> set if the message is a response +// | +// '-> set if the message is a request +// +// +// +// BYTE(0X01) BYTE(0X00) BYTE(0X00) BYTE(0X00) +// | +// '--> version +// +type Header struct { + Signature uint64 + Length uint64 + ExpectsResponse bool + Command uint32 + ReturnCode int32 + Flags uint32 // only 4 most significant bits matter (Q|S|B|E) + Version uint32 +} + +func NewRequestHeader(command uint32, length uint64) *Header { + return &Header{ + Signature: LevinSignature, + Length: length, + ExpectsResponse: true, + Command: command, + ReturnCode: 0, + Flags: LevinPacketRequest, + Version: LevinProtocolVersion, + } +} + +func NewHeaderFromBytesBytes(bytes []byte) (*Header, error) { + if len(bytes) != LevinHeaderSizeBytes { + return nil, fmt.Errorf("invalid header size: expected %d, has %d", + LevinHeaderSizeBytes, len(bytes), + ) + } + + var ( + size = 0 + idx = 0 + ) + + header := &Header{} + + { // signature + size = 8 + header.Signature = binary.LittleEndian.Uint64(bytes[idx : idx+size]) + idx += size + + if header.Signature != LevinSignature { + return nil, fmt.Errorf("signature mismatch: expected %x, got %x", + LevinSignature, header.Signature, + ) + } + } + + { // length + size = 8 + header.Length = binary.LittleEndian.Uint64(bytes[idx : idx+size]) + idx += size + } + + { // expects response + size = 1 + header.ExpectsResponse = (bytes[idx] != 0) + idx += size + } + + { // command + size = 4 + header.Command = binary.LittleEndian.Uint32(bytes[idx : idx+size]) + idx += size + + if !IsValidCommand(header.Command) { + return nil, fmt.Errorf("invalid command %d", header.Command) + } + } + + { // return code + size = 4 + header.ReturnCode = int32(binary.LittleEndian.Uint32(bytes[idx : idx+size])) + idx += size + + if !IsValidReturnCode(header.ReturnCode) { + return nil, fmt.Errorf("invalid return code %d", header.ReturnCode) + } + } + + { // flags + size = 4 + header.Flags = binary.LittleEndian.Uint32(bytes[idx : idx+size]) + idx += size + } + + { // version + size = 4 + header.Version = binary.LittleEndian.Uint32(bytes[idx : idx+size]) + idx += size + + if header.Version != LevinProtocolVersion { + return nil, fmt.Errorf("invalid version %x", + header.Version) + } + } + + return header, nil +} + +func (h *Header) Bytes() []byte { + var ( + header = make([]byte, LevinHeaderSizeBytes) // full header + b = make([]byte, 8) // biggest type + + idx = 0 + size = 0 + ) + + { // signature + size = 8 + + binary.LittleEndian.PutUint64(b, h.Signature) + copy(header[idx:], b[:size]) + idx += size + } + + { // length + size = 8 + + binary.LittleEndian.PutUint64(b, h.Length) + copy(header[idx:], b[:size]) + idx += size + } + + { // expects response + size = 1 + + if h.ExpectsResponse { + b[0] = 0x01 + } else { + b[0] = 0x00 + } + + copy(header[idx:], b[:size]) + idx += size + } + + { // command + size = 4 + + binary.LittleEndian.PutUint32(b, h.Command) + copy(header[idx:], b[:size]) + idx += size + } + + { // return code + size = 4 + + binary.LittleEndian.PutUint32(b, uint32(h.ReturnCode)) + copy(header[idx:], b[:size]) + idx += size + } + + { // flags + size = 4 + + binary.LittleEndian.PutUint32(b, h.Flags) + copy(header[idx:], b[:size]) + idx += size + } + + { // version + size = 4 + + binary.LittleEndian.PutUint32(b, h.Version) + copy(header[idx:], b[:size]) + idx += size + } + + return header +} diff --git a/monero/client/levin/levin_test.go b/monero/client/levin/levin_test.go new file mode 100644 index 0000000..15d5c98 --- /dev/null +++ b/monero/client/levin/levin_test.go @@ -0,0 +1,151 @@ +package levin_test + +import ( + "testing" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + "github.com/stretchr/testify/assert" + + "git.gammaspectra.live/P2Pool/consensus/v3/monero/client/levin" +) + +func TestLevin(t *testing.T) { + spec.Run(t, "NewHeaderFromBytes", func(t *testing.T, when spec.G, it spec.S) { + it("fails w/ wrong size", func() { + bytes := []byte{ + 0xff, + } + + _, err := levin.NewHeaderFromBytesBytes(bytes) + assert.Error(t, err) + }) + + it("fails w/ wrong signature", func() { + bytes := []byte{ + 0xff, 0xff, 0xff, 0xff, // signature + 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, // length + 0x00, 0x00, 0x00, 0x00, // + 0x00, // expects response + 0x00, 0x00, 0x00, 0x00, // command + 0x00, 0x00, 0x00, 0x00, // return code + 0x00, 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // version + } + + _, err := levin.NewHeaderFromBytesBytes(bytes) + assert.Error(t, err) + assert.Contains(t, err.Error(), "signature mismatch") + }) + + it("fails w/ invalid command", func() { + bytes := []byte{ + 0x01, 0x21, 0x01, 0x01, // signature + 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, // length + 0x00, 0x00, 0x00, 0x00, // + 0x01, // expects response + 0xff, 0xff, 0xff, 0xff, // command + 0x00, 0x00, 0x00, 0x00, // return code + 0x00, 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // version + } + + _, err := levin.NewHeaderFromBytesBytes(bytes) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid command") + }) + + it("fails w/ invalid return code", func() { + bytes := []byte{ + 0x01, 0x21, 0x01, 0x01, // signature + 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, // length + 0x00, 0x00, 0x00, 0x00, // + 0x01, // expects response + 0xe9, 0x03, 0x00, 0x00, // command + 0xaa, 0xaa, 0xaa, 0xaa, // return code + 0x00, 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // version + } + + _, err := levin.NewHeaderFromBytesBytes(bytes) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid return code") + }) + + it("fails w/ invalid version", func() { + bytes := []byte{ + 0x01, 0x21, 0x01, 0x01, // signature + 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, // length + 0x00, 0x00, 0x00, 0x00, // + 0x01, // expects response + 0xe9, 0x03, 0x00, 0x00, // command + 0x00, 0x00, 0x00, 0x00, // return code + 0x02, 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // version + } + + _, err := levin.NewHeaderFromBytesBytes(bytes) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid version") + }) + + it("assembles properly from pong", func() { + bytes := []byte{ + 0x01, 0x21, 0x01, 0x01, // signature + 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, // length + 0x00, 0x00, 0x00, 0x00, // + 0x01, // expects response + 0xeb, 0x03, 0x00, 0x00, // command + 0x00, 0x00, 0x00, 0x00, // return code + 0x02, 0x00, 0x00, 0x00, // flags + 0x01, 0x00, 0x00, 0x00, // version + } + + header, err := levin.NewHeaderFromBytesBytes(bytes) + assert.NoError(t, err) + assert.Equal(t, header.Command, levin.CommandPing) + assert.Equal(t, header.ReturnCode, levin.LevinOk) + assert.Equal(t, header.Flags, levin.LevinPacketReponse) + assert.Equal(t, header.Version, levin.LevinProtocolVersion) + }) + }) + + spec.Run(t, "NewRequestHeader", func(t *testing.T, when spec.G, it spec.S) { + it("assembles properly w/ ping", func() { + bytes := levin.NewRequestHeader(levin.CommandPing, 1).Bytes() + + assert.ElementsMatch(t, bytes, []byte{ + 0x01, 0x21, 0x01, 0x01, // signature + 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, // length -- 0 for a ping msg + 0x00, 0x00, 0x00, 0x00, + 0x01, // expects response -- `true` bool + 0xeb, 0x03, 0x00, 0x00, // command -- 1003 for ping + 0x00, 0x00, 0x00, 0x00, // return code -- 0 for requests + 0x01, 0x00, 0x00, 0x00, // flags -- Q(1st lsb) set for req + 0x01, 0x00, 0x00, 0x00, // version + }) + }) + + it("assembles properly w/ handshake", func() { + bytes := levin.NewRequestHeader(levin.CommandHandshake, 4).Bytes() + + assert.ElementsMatch(t, bytes, []byte{ + 0x01, 0x21, 0x01, 0x01, // signature + 0x01, 0x01, 0x01, 0x01, + 0x04, 0x00, 0x00, 0x00, // length -- 0 for a ping msg + 0x00, 0x00, 0x00, 0x00, + 0x01, // expects response -- `true` bool + 0xe9, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // return code -- 0 for requests + 0x01, 0x00, 0x00, 0x00, // flags -- Q(1st lsb) set for req + 0x01, 0x00, 0x00, 0x00, // version + }) + }) + }, spec.Report(report.Log{}), spec.Parallel(), spec.Random()) +} diff --git a/monero/client/levin/node.go b/monero/client/levin/node.go new file mode 100644 index 0000000..13df594 --- /dev/null +++ b/monero/client/levin/node.go @@ -0,0 +1,133 @@ +package levin + +import ( + "fmt" + "net" +) + +type Node struct { + Peers map[string]*Peer + + Id uint64 + RPCPort uint16 + + CurrentHeight uint64 + TopVersion uint8 +} + +func (l *Node) GetPeers() map[string]*Peer { + return l.Peers +} + +type Peer struct { + Ip string + Port uint16 +} + +func (p Peer) Addr() string { + return fmt.Sprintf("%s:%d", p.Ip, p.Port) +} + +func (p Peer) String() string { + return p.Addr() +} + +func ParsePeerList(entry Entry) map[string]*Peer { + peers := map[string]*Peer{} + + peerList := entry.Entries() + + for _, peer := range peerList { + peerListAdr := peer.Entries() + + for _, adr := range peerListAdr { + if adr.Name != "adr" { + continue + } + + addr := adr.Entries() + + for _, addrField := range addr { + if addrField.Name != "addr" { + continue + } + + fields := addrField.Entries() + + var ip string + var port uint16 + + for _, field := range fields { + if field.Name == "m_ip" { + ip = ipzify(field.Uint32()) + } + + if field.Name == "m_port" { + port = field.Uint16() + } + + if field.Name == "addr" { + ip = net.IP([]byte(field.String())).String() + } + } + + if ip != "" && port != 0 { + peer := &Peer{ + Ip: ip, + Port: port, + } + + peers[peer.Addr()] = peer + } + } + } + } + + return peers +} + +// TODO less panic'ing. +func NewNodeFromEntries(entries Entries) Node { + lpl := Node{} + + for _, entry := range entries { + if entry.Name == "node_data" { + for _, field := range entry.Entries() { + switch field.Name { + case "rpc_port": + lpl.RPCPort = field.Uint16() + case "peer_id": + lpl.Id = field.Uint64() + } + } + } + + if entry.Name == "payload_data" { + for _, field := range entry.Entries() { + switch field.Name { + case "current_height": + lpl.CurrentHeight = field.Uint64() + case "top_version": + lpl.TopVersion = field.Uint8() + } + } + } + + if entry.Name == "local_peerlist_new" { + lpl.Peers = ParsePeerList(entry) + } + } + + return lpl +} + +func ipzify(ip uint32) string { + result := make(net.IP, 4) + + result[0] = byte(ip) + result[1] = byte(ip >> 8) + result[2] = byte(ip >> 16) + result[3] = byte(ip >> 24) + + return result.String() +} diff --git a/monero/client/levin/portable_storage.go b/monero/client/levin/portable_storage.go new file mode 100644 index 0000000..b429cb9 --- /dev/null +++ b/monero/client/levin/portable_storage.go @@ -0,0 +1,410 @@ +package levin + +import ( + "encoding/binary" + "fmt" +) + +const ( + PortableStorageSignatureA uint32 = 0x01011101 + PortableStorageSignatureB uint32 = 0x01020101 + PortableStorageFormatVersion byte = 0x01 + + PortableRawSizeMarkMask byte = 0x03 + PortableRawSizeMarkByte byte = 0x00 + PortableRawSizeMarkWord uint16 = 0x01 + PortableRawSizeMarkDword uint32 = 0x02 + PortableRawSizeMarkInt64 uint64 = 0x03 +) + +type Entry struct { + Name string + Serializable Serializable `json:"-,omitempty"` + Value interface{} +} + +func (e Entry) String() string { + v, ok := e.Value.(string) + if !ok { + panic(fmt.Errorf("interface couldnt be casted to string")) + } + + return v +} + +func (e Entry) Uint8() uint8 { + v, ok := e.Value.(uint8) + if !ok { + panic(fmt.Errorf("interface couldnt be casted to uint8")) + } + + return v +} + +func (e Entry) Uint16() uint16 { + v, ok := e.Value.(uint16) + if !ok { + panic(fmt.Errorf("interface couldnt be casted to uint16")) + } + + return v +} + +func (e Entry) Uint32() uint32 { + v, ok := e.Value.(uint32) + if !ok { + panic(fmt.Errorf("interface couldnt be casted to uint32")) + } + + return v +} + +func (e Entry) Uint64() uint64 { + v, ok := e.Value.(uint64) + if !ok { + panic(fmt.Errorf("interface couldnt be casted to uint64")) + } + + return v +} + +func (e Entry) Entries() Entries { + v, ok := e.Value.(Entries) + if !ok { + panic(fmt.Errorf("interface couldnt be casted to levin.Entries")) + } + + return v +} + +func (e Entry) Bytes() []byte { + return nil +} + +type Entries []Entry + +func (e Entries) Bytes() []byte { + return nil +} + +type PortableStorage struct { + Entries Entries +} + +func NewPortableStorageFromBytes(bytes []byte) (*PortableStorage, error) { + var ( + size = 0 + idx = 0 + ) + + { // sig-a + size = 4 + + if len(bytes[idx:]) < size { + return nil, fmt.Errorf("sig-a out of bounds") + } + + sig := binary.LittleEndian.Uint32(bytes[idx : idx+size]) + idx += size + + if sig != uint32(PortableStorageSignatureA) { + return nil, fmt.Errorf("sig-a doesn't match") + } + } + + { // sig-b + size = 4 + sig := binary.LittleEndian.Uint32(bytes[idx : idx+size]) + idx += size + + if sig != uint32(PortableStorageSignatureB) { + return nil, fmt.Errorf("sig-b doesn't match") + } + } + + { // format ver + size = 1 + version := bytes[idx] + idx += size + + if version != PortableStorageFormatVersion { + return nil, fmt.Errorf("version doesn't match") + } + } + + ps := &PortableStorage{} + + _, ps.Entries = ReadObject(bytes[idx:]) + + return ps, nil +} + +func ReadString(bytes []byte) (int, string) { + idx := 0 + + n, strLen := ReadVarInt(bytes) + idx += n + + return idx + strLen, string(bytes[idx : idx+strLen]) +} + +func ReadObject(bytes []byte) (int, Entries) { + idx := 0 + + n, i := ReadVarInt(bytes[idx:]) + idx += n + + entries := make(Entries, i) + + for iter := 0; iter < i; iter++ { + entries[iter] = Entry{} + entry := &entries[iter] + + lenName := int(bytes[idx]) + idx += 1 + + entry.Name = string(bytes[idx : idx+lenName]) + idx += lenName + + ttype := bytes[idx] + idx += 1 + + n, obj := ReadAny(bytes[idx:], ttype) + idx += n + + entry.Value = obj + } + + return idx, entries +} + +func ReadArray(ttype byte, bytes []byte) (int, Entries) { + var ( + idx = 0 + n = 0 + ) + + n, i := ReadVarInt(bytes[idx:]) + idx += n + + entries := make(Entries, i) + + for iter := 0; iter < i; iter++ { + n, obj := ReadAny(bytes[idx:], ttype) + idx += n + + entries[iter] = Entry{ + Value: obj, + } + } + + return idx, entries +} + +func ReadAny(bytes []byte, ttype byte) (int, interface{}) { + var ( + idx = 0 + n = 0 + ) + + if ttype&BoostSerializeFlagArray != 0 { + internalType := ttype &^ BoostSerializeFlagArray + n, obj := ReadArray(internalType, bytes[idx:]) + idx += n + + return idx, obj + } + + if ttype == BoostSerializeTypeObject { + n, obj := ReadObject(bytes[idx:]) + idx += n + + return idx, obj + } + + if ttype == BoostSerializeTypeUint8 { + obj := uint8(bytes[idx]) + n += 1 + idx += n + + return idx, obj + } + + if ttype == BoostSerializeTypeUint16 { + obj := binary.LittleEndian.Uint16(bytes[idx:]) + n += 2 + idx += n + + return idx, obj + } + + if ttype == BoostSerializeTypeUint32 { + obj := binary.LittleEndian.Uint32(bytes[idx:]) + n += 4 + idx += n + + return idx, obj + } + + if ttype == BoostSerializeTypeUint64 { + obj := binary.LittleEndian.Uint64(bytes[idx:]) + n += 8 + idx += n + + return idx, obj + } + + if ttype == BoostSerializeTypeInt64 { + obj := binary.LittleEndian.Uint64(bytes[idx:]) + n += 8 + idx += n + + return idx, int64(obj) + } + + if ttype == BoostSerializeTypeString { + n, obj := ReadString(bytes[idx:]) + idx += n + + return idx, obj + } + + if ttype == BoostSerializeTypeBool { + obj := bytes[idx] > 0 + n += 1 + idx += n + + return idx, obj + } + + panic(fmt.Errorf("unknown ttype %x", ttype)) + return -1, nil +} + +// reads var int, returning number of bytes read and the integer in that byte +// sequence. +func ReadVarInt(b []byte) (int, int) { + sizeMask := b[0] & PortableRawSizeMarkMask + + switch uint32(sizeMask) { + case uint32(PortableRawSizeMarkByte): + return 1, int(b[0] >> 2) + case uint32(PortableRawSizeMarkWord): + return 2, int((binary.LittleEndian.Uint16(b[0:2])) >> 2) + case PortableRawSizeMarkDword: + return 4, int((binary.LittleEndian.Uint32(b[0:4])) >> 2) + case uint32(PortableRawSizeMarkInt64): + panic("int64 not supported") // TODO + // return int((binary.LittleEndian.Uint64(b[0:8])) >> 2) + // '-> bad + default: + panic(fmt.Errorf("malformed sizemask: %+v", sizeMask)) + } + + return -1, -1 +} + +func (s *PortableStorage) Bytes() []byte { + var ( + body = make([]byte, 9) // fit _at least_ signatures + format ver + b = make([]byte, 8) // biggest type + + idx = 0 + size = 0 + ) + + { // signature a + size = 4 + + binary.LittleEndian.PutUint32(b, PortableStorageSignatureA) + copy(body[idx:], b[:size]) + idx += size + } + + { // signature b + size = 4 + + binary.LittleEndian.PutUint32(b, PortableStorageSignatureB) + copy(body[idx:], b[:size]) + idx += size + } + + { // format ver + size = 1 + + b[0] = PortableStorageFormatVersion + copy(body[idx:], b[:size]) + idx += size + } + + // // write_var_in + varInB, err := VarIn(len(s.Entries)) + if err != nil { + panic(fmt.Errorf("varin '%d': %w", len(s.Entries), err)) + } + + body = append(body, varInB...) + for _, entry := range s.Entries { + body = append(body, byte(len(entry.Name))) // section name length + body = append(body, []byte(entry.Name)...) // section name + body = append(body, entry.Serializable.Bytes()...) + } + + return body +} + +type Serializable interface { + Bytes() []byte +} + +type Section struct { + Entries []Entry +} + +func (s Section) Bytes() []byte { + body := []byte{ + BoostSerializeTypeObject, + } + + varInB, err := VarIn(len(s.Entries)) + if err != nil { + panic(fmt.Errorf("varin '%d': %w", len(s.Entries), err)) + } + + body = append(body, varInB...) + for _, entry := range s.Entries { + body = append(body, byte(len(entry.Name))) // section name length + body = append(body, []byte(entry.Name)...) // section name + body = append(body, entry.Serializable.Bytes()...) + } + + return body +} + +func VarIn(i int) ([]byte, error) { + if i <= 63 { + return []byte{ + (byte(i) << 2) | PortableRawSizeMarkByte, + }, nil + } + + if i <= 16383 { + b := []byte{0x00, 0x00} + binary.LittleEndian.PutUint16(b, + (uint16(i)<<2)|PortableRawSizeMarkWord, + ) + + return b, nil + } + + if i <= 1073741823 { + b := []byte{0x00, 0x00, 0x00, 0x00} + binary.LittleEndian.PutUint32(b, + (uint32(i)<<2)|PortableRawSizeMarkDword, + ) + + return b, nil + } + + return nil, fmt.Errorf("int %d too big", i) +} diff --git a/monero/client/levin/portable_storage_test.go b/monero/client/levin/portable_storage_test.go new file mode 100644 index 0000000..566c13e --- /dev/null +++ b/monero/client/levin/portable_storage_test.go @@ -0,0 +1,225 @@ +package levin_test + +import ( + "testing" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + "github.com/stretchr/testify/assert" + + "git.gammaspectra.live/P2Pool/consensus/v3/monero/client/levin" +) + +func TestPortableStorage(t *testing.T) { + spec.Run(t, "NewPortableStorageFromBytes", func(t *testing.T, when spec.G, it spec.S) { + it("fails w/ wrong sigA", func() { + bytes := []byte{ + 0xaa, 0xaa, 0xaa, 0xaa, + } + + _, err := levin.NewPortableStorageFromBytes(bytes) + assert.Error(t, err) + assert.Contains(t, err.Error(), "sig-a doesn't match") + }) + + it("fails w/ wrong sigB", func() { + bytes := []byte{ + 0x01, 0x11, 0x01, 0x01, + 0xaa, 0xaa, 0xaa, 0xaa, + } + + _, err := levin.NewPortableStorageFromBytes(bytes) + assert.Error(t, err) + assert.Contains(t, err.Error(), "sig-b doesn't match") + }) + + it("fails w/ wrong format ver", func() { + bytes := []byte{ + 0x01, 0x11, 0x01, 0x01, + 0x01, 0x01, 0x02, 0x01, + 0xaa, + } + + _, err := levin.NewPortableStorageFromBytes(bytes) + assert.Error(t, err) + assert.Contains(t, err.Error(), "version doesn't match") + }) + + it("reads the contents", func() { + bytes := []byte{ + 0x01, 0x11, 0x01, 0x01, // sig a + 0x01, 0x01, 0x02, 0x01, // sig b + 0x01, // format ver + + 0x08, // var_in(len(entries)) + + // node_data + 0x09, // len("node_data") + 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x64, 0x61, 0x74, 0x61, // "node_data" + 0x0c, // boost_serialized_obj + 0x04, // var_in(node_data.entries) + + // for i in range node_data + 0x03, // len("foo") + 0x66, 0x6f, 0x6f, // "foo" + 0x0a, // boost_serialized_string + 0xc, // var_in(len("bar")) + 0x62, 0x61, 0x72, // "bar" + + // payload_data + 0x0c, // len("payload_data") + 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, // "payload_data" + 0x0c, // boost_serialized_obj + 0x04, // var_in(payload_data.entries) + + // for i in range payload_data.entries + 0x06, // len("number") + 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, // "number" + 0x06, // boost_serialized_uint32 + 0x01, 0x00, 0x00, 0x00, // uint32(1) + } + + ps, err := levin.NewPortableStorageFromBytes(bytes) + assert.NoError(t, err) + + assert.Len(t, ps.Entries, 2) + assert.Equal(t, ps.Entries[0].Name, "node_data") + assert.EqualValues(t, ps.Entries[0].Value, levin.Entries{ + { + Name: "foo", + Value: "bar", + }, + }) + + assert.Equal(t, ps.Entries[1].Name, "payload_data") + assert.EqualValues(t, ps.Entries[1].Value, levin.Entries{ + { + Name: "number", + Value: uint32(1), + }, + }) + }) + }, spec.Report(report.Log{}), spec.Parallel(), spec.Random()) + + spec.Run(t, "ReadVarIn", func(t *testing.T, when spec.G, it spec.S) { + it("i <= 63", func() { + b := []byte{0x08} + n, v := levin.ReadVarInt(b) + + assert.Equal(t, n, 1) + assert.Equal(t, v, 2) + }) + + it("64 <= i <= 16383", func() { + b := []byte{0x01, 0x02} + n, v := levin.ReadVarInt(b) + assert.Equal(t, n, 2) + assert.Equal(t, v, 128) + }) + + it("16384 <= i <= 1073741823", func() { + b := []byte{0x02, 0x00, 0x01, 0x00} + n, v := levin.ReadVarInt(b) + assert.Equal(t, n, 4) + assert.Equal(t, v, 16384) + }) + }, spec.Report(report.Log{}), spec.Parallel(), spec.Random()) + + spec.Run(t, "VarrIn", func(t *testing.T, when spec.G, it spec.S) { + it("i <= 63", func() { + i := 2 // 0b00000010 + + b, err := levin.VarIn(i) + assert.NoError(t, err) + assert.Equal(t, b, []byte{ + 0x08, // 0b00001000 (shift left twice, union 0) + }) + }) + + it("64 <= i <= 16383", func() { + i := 128 // 0b010000000 + + b, err := levin.VarIn(i) + assert.NoError(t, err) + assert.Equal(t, b, []byte{ + 0x01, 0x02, // 0b1000000001 ((128 * 2 * 2) | 1) == 513 + // ' ' + // 1 2 * 256 + }) + }) + + it("16384 <= i <= 1073741823", func() { + i := 16384 // 1 << 14 + + b, err := levin.VarIn(i) + assert.NoError(t, err) + assert.Equal(t, b, []byte{ + 0x02, 0x00, 0x01, 0x00, // (1 << 16) | 2 + }) + }) + }, spec.Report(report.Log{}), spec.Parallel(), spec.Random()) + + spec.Run(t, "PortableStorage", func(t *testing.T, when spec.G, it spec.S) { + it("bytes", func() { + ps := &levin.PortableStorage{ + Entries: []levin.Entry{ + { + Name: "node_data", + Serializable: &levin.Section{ + Entries: []levin.Entry{ + { + Name: "foo", + Serializable: levin.BoostString("bar"), + }, + }, + }, + }, + { + Name: "payload_data", + Serializable: &levin.Section{ + Entries: []levin.Entry{ + { + Name: "number", + Serializable: levin.BoostUint32(1), + }, + }, + }, + }, + }, + } + + assert.Equal(t, []byte{ + 0x01, 0x11, 0x01, 0x01, // sig a + 0x01, 0x01, 0x02, 0x01, // sig b + 0x01, // format ver + 0x08, // var_in(len(entries)) + + // node_data + 0x09, // len("node_data") + 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x64, 0x61, 0x74, 0x61, // "node_data" + 0x0c, // boost_serialized_obj + 0x04, // var_in(node_data.entries) + + // for i in range node_data + 0x03, // len("foo") + 0x66, 0x6f, 0x6f, // "foo" + 0x0a, // boost_serialized_string + 0xc, // var_in(len("bar")) + 0x62, 0x61, 0x72, // "bar" + + // payload_data + 0x0c, // len("payload_data") + 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, // "payload_data" + 0x0c, // boost_serialized_obj + 0x04, // var_in(payload_data.entries) + + // for i in range payload_data.entries + 0x06, // len("number") + 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, // "number" + 0x06, // boost_serialized_uint32 + 0x01, 0x00, 0x00, 0x00, // uint32(1) + + }, ps.Bytes()) + }) + }, spec.Report(report.Log{}), spec.Parallel(), spec.Random()) +} diff --git a/monero/client/rpc/LICENSE b/monero/client/rpc/LICENSE new file mode 100644 index 0000000..ffdef91 --- /dev/null +++ b/monero/client/rpc/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 Ciro S. Costa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/monero/client/rpc/README.md b/monero/client/rpc/README.md new file mode 100644 index 0000000..611775a --- /dev/null +++ b/monero/client/rpc/README.md @@ -0,0 +1,3 @@ +# go-monero RPC + +Taken from [git.gammaspectra.live/P2Pool/go-monero](https://git.gammaspectra.live/P2Pool/go-monero/src/commit/910450c4a523a8a21c0bcabf7d303418e6c76d50/pkg/rpc) \ No newline at end of file diff --git a/monero/client/rpc/client.go b/monero/client/rpc/client.go new file mode 100644 index 0000000..3490571 --- /dev/null +++ b/monero/client/rpc/client.go @@ -0,0 +1,223 @@ +package rpc + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +const ( + // endpointJSONRPC is the common endpoint used for all the RPC calls + // that make use of epee's JSONRPC invocation format for requests and + // responses. + // + endpointJSONRPC = "/json_rpc" + + // versionJSONRPC is the version of the JSONRPC format. + // + versionJSONRPC = "2.0" +) + +// Client is a wrapper over a plain HTTP client providing methods that +// correspond to all RPC invocations to a `monerod` daemon, including +// restricted and non-restricted ones. +type Client struct { + // http is the underlying http client that takes care of sending + // requests and receiving the responses. + // + // To provide your own, make use of `WithHTTPClient` when instantiating + // the client via the `NewClient` constructor. + // + http *http.Client + + // address is the address of the monerod instance serving the RPC + // endpoints. + // + address *url.URL +} + +// clientOptions is a set of options that can be overridden to tweak the +// client's behavior. +type clientOptions struct { + HTTPClient *http.Client +} + +// ClientOption defines a functional option for overriding optional client +// configuration parameters. +type ClientOption func(o *clientOptions) + +// WithHTTPClient is a functional option for providing a custom HTTP client to +// be used for the HTTP requests made to a monero daemon. +func WithHTTPClient(v *http.Client) func(o *clientOptions) { + return func(o *clientOptions) { + o.HTTPClient = v + } +} + +// NewClient instantiates a new Client that is able to communicate with +// monerod's RPC endpoints. +// +// The `address` might be either restricted (typically :18089) or not +// (typically :18081). +func NewClient(address string, opts ...ClientOption) (*Client, error) { + options := &clientOptions{} + + for _, opt := range opts { + opt(options) + } + + if options.HTTPClient == nil { + options.HTTPClient = http.DefaultClient + } + + parsedAddress, err := url.Parse(address) + if err != nil { + return nil, fmt.Errorf("url parse: %w", err) + } + + return &Client{ + address: parsedAddress, + http: options.HTTPClient, + }, nil +} + +// ResponseEnvelope wraps all responses from the RPC server. +type ResponseEnvelope struct { + ID string `json:"id"` + JSONRPC string `json:"jsonrpc"` + Result interface{} `json:"result,omitempty"` + Error struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error,omitempty"` +} + +// RequestEnvelope wraps all requests made to the RPC server. +type RequestEnvelope struct { + ID string `json:"id"` + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params interface{} `json:"params,omitempty"` +} + +// RawBinaryRequest makes requests to any endpoints, not assuming any particular format. +func (c *Client) RawBinaryRequest(ctx context.Context, endpoint string, body io.Reader) (io.ReadCloser, error) { + address := *c.address + address.Path = endpoint + + req, err := http.NewRequestWithContext(ctx, "POST", address.String(), body) + if err != nil { + return nil, fmt.Errorf("new req '%s': %w", address.String(), err) + } + + req.Header.Add("Content-Type", "application/octet-stream") + + resp, err := c.http.Do(req) + if err != nil { + return nil, fmt.Errorf("do: %w", err) + } + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + defer resp.Body.Close() + return nil, fmt.Errorf("non-2xx status code: %d", resp.StatusCode) + } + + return resp.Body, nil +} + +// RawRequest makes requests to any endpoints, not assuming any particular format except of response is JSON. +func (c *Client) RawRequest(ctx context.Context, endpoint string, params interface{}, response interface{}) error { + address := *c.address + address.Path = endpoint + + var body io.Reader + + if params != nil { + b, err := json.Marshal(params) + if err != nil { + return fmt.Errorf("marshal: %w", err) + } + + body = bytes.NewReader(b) + } + + req, err := http.NewRequestWithContext(ctx, "GET", address.String(), body) + if err != nil { + return fmt.Errorf("new req '%s': %w", address.String(), err) + } + + req.Header.Add("Content-Type", "application/json") + + if err := c.submitRequest(req, response); err != nil { + return fmt.Errorf("submit request: %w", err) + } + + return nil +} + +// JSONRPC issues a request for a particular method under the JSONRPC endpoint +// with the proper envolope for its requests and unwrapping of results for +// responses. +func (c *Client) JSONRPC(ctx context.Context, method string, params interface{}, response interface{}) error { + address := *c.address + address.Path = endpointJSONRPC + + b, err := json.Marshal(&RequestEnvelope{ + ID: "0", + JSONRPC: versionJSONRPC, + Method: method, + Params: params, + }) + if err != nil { + return fmt.Errorf("marshal: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", address.String(), bytes.NewReader(b)) + if err != nil { + return fmt.Errorf("new req '%s': %w", address.String(), err) + } + + req.Header.Add("Content-Type", "application/json") + + rpcResponseBody := &ResponseEnvelope{ + Result: response, + } + + if err := c.submitRequest(req, rpcResponseBody); err != nil { + return fmt.Errorf("submit request: %w", err) + } + + if rpcResponseBody.Error.Code != 0 || rpcResponseBody.Error.Message != "" { + return fmt.Errorf("rpc error: code=%d message=%s", + rpcResponseBody.Error.Code, + rpcResponseBody.Error.Message, + ) + } + + return nil +} + +// submitRequest performs any generic HTTP request to the monero node targeted +// by this client making no assumptions about a particular endpoint. +func (c *Client) submitRequest(req *http.Request, response interface{}) error { + resp, err := c.http.Do(req) + if err != nil { + return fmt.Errorf("do: %w", err) + } + + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return fmt.Errorf("non-2xx status code: %d", resp.StatusCode) + } + + if err := json.NewDecoder(resp.Body).Decode(response); err != nil { + return fmt.Errorf("decode: %w", err) + } + + return nil +} diff --git a/monero/client/rpc/client_export_test.go b/monero/client/rpc/client_export_test.go new file mode 100644 index 0000000..ab6d00b --- /dev/null +++ b/monero/client/rpc/client_export_test.go @@ -0,0 +1,3 @@ +package rpc + +var EndpointJSONRPC = endpointJSONRPC diff --git a/monero/client/rpc/client_test.go b/monero/client/rpc/client_test.go new file mode 100644 index 0000000..f3a097c --- /dev/null +++ b/monero/client/rpc/client_test.go @@ -0,0 +1,159 @@ +package rpc_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "git.gammaspectra.live/P2Pool/consensus/v3/monero/client/rpc" +) + +// nolint:funlen +func TestClient(t *testing.T) { + spec.Run(t, "JSONRPC", func(t *testing.T, when spec.G, it spec.S) { + var ( + ctx = context.Background() + client *rpc.Client + err error + ) + + it("errors when daemon down", func() { + daemon := httptest.NewServer(http.HandlerFunc(nil)) + daemon.Close() + + client, err = rpc.NewClient(daemon.URL, rpc.WithHTTPClient(daemon.Client())) + require.NoError(t, err) + + err = client.JSONRPC(ctx, "method", nil, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "do:") + }) + + it("errors w/ empty response", func() { + handler := func(w http.ResponseWriter, r *http.Request) {} + + daemon := httptest.NewServer(http.HandlerFunc(handler)) + defer daemon.Close() + + client, err = rpc.NewClient(daemon.URL, rpc.WithHTTPClient(daemon.Client())) + require.NoError(t, err) + + err = client.JSONRPC(ctx, "method", nil, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "decode") + }) + + it("errors w/ non-200 response", func() { + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + } + + daemon := httptest.NewServer(http.HandlerFunc(handler)) + defer daemon.Close() + + client, err = rpc.NewClient(daemon.URL, rpc.WithHTTPClient(daemon.Client())) + require.NoError(t, err) + + err = client.JSONRPC(ctx, "method", nil, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "non-2xx status") + }) + + it("makes GET request to the jsonrpc endpoint", func() { + var ( + endpoint string + method string + ) + + handler := func(w http.ResponseWriter, r *http.Request) { + endpoint = r.URL.Path + method = r.Method + } + + daemon := httptest.NewServer(http.HandlerFunc(handler)) + defer daemon.Close() + + client, err = rpc.NewClient(daemon.URL, rpc.WithHTTPClient(daemon.Client())) + require.NoError(t, err) + + err = client.JSONRPC(ctx, "method", nil, nil) + assert.Equal(t, rpc.EndpointJSONRPC, endpoint) + assert.Equal(t, method, "GET") + }) + + it("encodes rpc in request", func() { + var ( + body = &rpc.RequestEnvelope{} + + params = map[string]interface{}{ + "foo": "bar", + "caz": 123.123, + } + ) + + handler := func(w http.ResponseWriter, r *http.Request) { + err := json.NewDecoder(r.Body).Decode(body) + assert.NoError(t, err) + } + + daemon := httptest.NewServer(http.HandlerFunc(handler)) + defer daemon.Close() + + client, err = rpc.NewClient(daemon.URL, rpc.WithHTTPClient(daemon.Client())) + require.NoError(t, err) + + err = client.JSONRPC(ctx, "rpc-method", params, nil) + assert.Equal(t, body.ID, "0") + assert.Equal(t, body.JSONRPC, "2.0") + assert.Equal(t, body.Method, "rpc-method") + assert.Equal(t, body.Params, params) + }) + + it("captures result", func() { + handler := func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"id":"id", "jsonrpc":"jsonrpc", "result": {"foo": "bar"}}`) + } + + daemon := httptest.NewServer(http.HandlerFunc(handler)) + defer daemon.Close() + + client, err = rpc.NewClient(daemon.URL, rpc.WithHTTPClient(daemon.Client())) + require.NoError(t, err) + + result := map[string]string{} + + err = client.JSONRPC(ctx, "rpc-method", nil, &result) + assert.NoError(t, err) + + assert.Equal(t, result, map[string]string{"foo": "bar"}) + }) + + it("fails if rpc errored", func() { + handler := func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"id":"id", "jsonrpc":"jsonrpc", "error": {"code": -1, "message":"foo"}}`) + } + + daemon := httptest.NewServer(http.HandlerFunc(handler)) + defer daemon.Close() + + client, err = rpc.NewClient(daemon.URL, rpc.WithHTTPClient(daemon.Client())) + require.NoError(t, err) + + result := map[string]string{} + + err = client.JSONRPC(ctx, "rpc-method", nil, &result) + assert.Error(t, err) + + assert.Contains(t, err.Error(), "foo") + assert.Contains(t, err.Error(), "-1") + }) + }, spec.Report(report.Terminal{}), spec.Parallel(), spec.Random()) +} diff --git a/monero/client/rpc/daemon/binary_endpoints.go b/monero/client/rpc/daemon/binary_endpoints.go new file mode 100644 index 0000000..d93c2c4 --- /dev/null +++ b/monero/client/rpc/daemon/binary_endpoints.go @@ -0,0 +1,69 @@ +package daemon + +import ( + "bytes" + "context" + "encoding/hex" + "errors" + "git.gammaspectra.live/P2Pool/consensus/v3/monero/client/levin" + "io" +) + +const ( + endpointGetOIndexes = "/get_o_indexes.bin" +) + +func (c *Client) GetOIndexes( + ctx context.Context, txid string, +) (indexes []uint64, finalError error) { + + binaryTxId, err := hex.DecodeString(txid) + if err != nil { + return nil, err + } + + storage := levin.PortableStorage{Entries: levin.Entries{ + levin.Entry{ + Name: "txid", + Serializable: levin.BoostString(binaryTxId), + }, + }} + + data := storage.Bytes() + + resp, err := c.RawBinaryRequest(ctx, endpointGetOIndexes, bytes.NewReader(data)) + if err != nil { + return nil, err + } + defer resp.Close() + + if buf, err := io.ReadAll(resp); err != nil { + return nil, err + } else { + defer func() { + if r := recover(); r != nil { + indexes = nil + finalError = errors.New("error decoding") + } + }() + responseStorage, err := levin.NewPortableStorageFromBytes(buf) + if err != nil { + return nil, err + } + for _, e := range responseStorage.Entries { + if e.Name == "o_indexes" { + if entries, ok := e.Value.(levin.Entries); ok { + indexes = make([]uint64, 0, len(entries)) + for _, e2 := range entries { + if v, ok := e2.Value.(uint64); ok { + indexes = append(indexes, v) + } + } + return indexes, nil + } + } + } + } + + return nil, errors.New("could not get outputs") +} diff --git a/monero/client/rpc/daemon/client.go b/monero/client/rpc/daemon/client.go new file mode 100644 index 0000000..a84b68d --- /dev/null +++ b/monero/client/rpc/daemon/client.go @@ -0,0 +1,51 @@ +package daemon + +import ( + "context" + "io" +) + +// Requester is responsible for making concrete request to Monero's endpoints, +// i.e., either `jsonrpc` methods or those "raw" endpoints. +type Requester interface { + // JSONRPC is used for callind methods under `/json_rpc` that follow + // monero's `v2` response and error encapsulation. + // + JSONRPC( + ctx context.Context, method string, params, result interface{}, + ) error + + // RawRequest is used for making a request to an arbitrary endpoint + // `endpoint` whose response (in JSON format) should be unmarshalled to + // `response`. + // + RawRequest( + ctx context.Context, + endpoint string, + params interface{}, + response interface{}, + ) error + + // RawBinaryRequest is used for making a request to an arbitrary endpoint + // `endpoint` whose response will be returned (which MUST be closed in error = nil) + // + RawBinaryRequest( + ctx context.Context, + endpoint string, + body io.Reader, + ) (io.ReadCloser, error) +} + +// Client provides access to the daemon's JSONRPC methods and regular +// endpoints. +type Client struct { + Requester +} + +// NewClient instantiates a new client for interacting with monero's daemon +// api. +func NewClient(c Requester) *Client { + return &Client{ + Requester: c, + } +} diff --git a/monero/client/rpc/daemon/daemon_example_test.go b/monero/client/rpc/daemon/daemon_example_test.go new file mode 100644 index 0000000..bb88aed --- /dev/null +++ b/monero/client/rpc/daemon/daemon_example_test.go @@ -0,0 +1,32 @@ +package daemon_test + +import ( + "context" + "fmt" + + "git.gammaspectra.live/P2Pool/consensus/v3/monero/client/rpc" + "git.gammaspectra.live/P2Pool/consensus/v3/monero/client/rpc/daemon" +) + +// nolint +func ExampleGetHeight() { + ctx := context.Background() + addr := "http://localhost:18081" + + // instantiate a generic RPC client + // + client, err := rpc.NewClient(addr) + if err != nil { + panic(fmt.Errorf("new client for '%s': %w", addr, err)) + } + + // instantiate a daemon-specific client and call the `get_height` + // remote procedure. + // + height, err := daemon.NewClient(client).GetHeight(ctx) + if err != nil { + panic(fmt.Errorf("get height: %w", err)) + } + + fmt.Printf("height=%d hash=%s\n", height.Height, height.Hash) +} diff --git a/monero/client/rpc/daemon/doc.go b/monero/client/rpc/daemon/doc.go new file mode 100644 index 0000000..c41925a --- /dev/null +++ b/monero/client/rpc/daemon/doc.go @@ -0,0 +1,4 @@ +// Package daemon provides a client that encapsulates RPC methods and endpoints +// that can be hit in a daemon. +// +package daemon diff --git a/monero/client/rpc/daemon/jsonrpc.go b/monero/client/rpc/daemon/jsonrpc.go new file mode 100644 index 0000000..21fdecb --- /dev/null +++ b/monero/client/rpc/daemon/jsonrpc.go @@ -0,0 +1,432 @@ +package daemon + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" +) + +const ( + methodGenerateBlocks = "generateblocks" + methodGetAlternateChains = "get_alternate_chains" + methodGetBans = "get_bans" + methodGetBlock = "get_block" + methodGetBlockCount = "get_block_count" + methodGetBlockHeadersRange = "get_block_headers_range" + methodGetBlockHeaderByHash = "get_block_header_by_hash" + methodGetBlockHeaderByHeight = "get_block_header_by_height" + methodGetBlockTemplate = "get_block_template" + methodGetMinerData = "get_miner_data" + methodGetCoinbaseTxSum = "get_coinbase_tx_sum" + methodGetConnections = "get_connections" + methodGetFeeEstimate = "get_fee_estimate" + methodGetInfo = "get_info" + methodGetLastBlockHeader = "get_last_block_header" + methodGetVersion = "get_version" + methodHardForkInfo = "hard_fork_info" + methodOnGetBlockHash = "on_get_block_hash" + methodRPCAccessTracking = "rpc_access_tracking" + methodRelayTx = "relay_tx" + methodSetBans = "set_bans" + methodSyncInfo = "sync_info" + methodSubmitBlock = "submit_block" +) + +// GetAlternateChains displays alternative chains seen by the node. +// +// (restricted). +func (c *Client) GetAlternateChains( + ctx context.Context, +) (*GetAlternateChainsResult, error) { + resp := &GetAlternateChainsResult{} + + err := c.JSONRPC(ctx, methodGetAlternateChains, nil, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +// RPCAccessTracking retrieves statistics that the monero daemon keeps track of +// about the use of each RPC method and endpoint. +// +// (restricted). +func (c *Client) RPCAccessTracking( + ctx context.Context, +) (*RPCAccessTrackingResult, error) { + resp := &RPCAccessTrackingResult{} + + err := c.JSONRPC(ctx, methodRPCAccessTracking, nil, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +// HardForkInfo looks up informaiton about the last hard fork. +func (c *Client) HardForkInfo( + ctx context.Context, +) (*HardForkInfoResult, error) { + resp := &HardForkInfoResult{} + + err := c.JSONRPC(ctx, methodHardForkInfo, nil, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +// GetBans retrieves the list of banned IPs. +// +// (restricted). +func (c *Client) GetBans(ctx context.Context) (*GetBansResult, error) { + resp := &GetBansResult{} + + err := c.JSONRPC(ctx, methodGetBans, nil, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +type SetBansBan struct { + Host string `json:"host"` + Ban bool `json:"ban"` + Seconds int64 `json:"seconds"` +} + +type SetBansRequestParameters struct { + Bans []SetBansBan `json:"bans"` +} + +// SetBans bans a particular host. +// +// (restricted). +func (c *Client) SetBans( + ctx context.Context, params SetBansRequestParameters, +) (*SetBansResult, error) { + resp := &SetBansResult{} + + err := c.JSONRPC(ctx, methodSetBans, params, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +// GetVersion retrieves the version of monerod that the node uses. +// +// (restricted). +func (c *Client) GetVersion(ctx context.Context) (*GetVersionResult, error) { + resp := &GetVersionResult{} + + err := c.JSONRPC(ctx, methodGetVersion, nil, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +// GenerateBlocksRequestParameters is the set of parameters to be passed to the +// GenerateBlocks RPC method. +type GenerateBlocksRequestParameters struct { + // AmountOfBlocks is the number of blocks to be generated. + // + AmountOfBlocks uint64 `json:"amount_of_blocks,omitempty"` + + // WalletAddress is the address of the wallet that will get the rewards + // of the coinbase transaction for such the blocks generates. + // + WalletAddress string `json:"wallet_address,omitempty"` + + // PreviousBlock TODO + // + PreviousBlock string `json:"prev_block,omitempty"` + + // StartingNonce TODO + // + StartingNonce uint32 `json:"starting_nonce,omitempty"` +} + +// GenerateBlocks combines functionality from `GetBlockTemplate` and +// `SubmitBlock` RPC calls to allow rapid block creation. +// +// Difficulty is set permanently to 1 for regtest. +// +// (restricted). +func (c *Client) GenerateBlocks( + ctx context.Context, params GenerateBlocksRequestParameters, +) (*GenerateBlocksResult, error) { + resp := &GenerateBlocksResult{} + + err := c.JSONRPC(ctx, methodGenerateBlocks, params, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +func (c *Client) GetBlockCount( + ctx context.Context, +) (*GetBlockCountResult, error) { + resp := &GetBlockCountResult{} + + err := c.JSONRPC(ctx, methodGetBlockCount, nil, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +func (c *Client) OnGetBlockHash( + ctx context.Context, height uint64, +) (string, error) { + resp := "" + params := []uint64{height} + + err := c.JSONRPC(ctx, methodOnGetBlockHash, params, &resp) + if err != nil { + return "", fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +func (c *Client) RelayTx( + ctx context.Context, txns []string, +) (*RelayTxResult, error) { + resp := &RelayTxResult{} + params := map[string]interface{}{ + "txids": txns, + } + + err := c.JSONRPC(ctx, methodRelayTx, params, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +// GetBlockTemplate gets a block template on which mining a new block. +func (c *Client) GetBlockTemplate( + ctx context.Context, walletAddress string, reserveSize uint, +) (*GetBlockTemplateResult, error) { + resp := &GetBlockTemplateResult{} + params := map[string]interface{}{ + "wallet_address": walletAddress, + "reserve_size": reserveSize, + } + + err := c.JSONRPC(ctx, methodGetBlockTemplate, params, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +func (c *Client) GetMinerData(ctx context.Context) (*GetMinerDataResult, error) { + resp := &GetMinerDataResult{} + + err := c.JSONRPC(ctx, methodGetMinerData, nil, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +func (c *Client) SubmitBlock(ctx context.Context, blobs ...[]byte) (*SubmitBlockResult, error) { + resp := &SubmitBlockResult{} + + params := make([]string, 0, len(blobs)) + for _, blob := range blobs { + params = append(params, hex.EncodeToString(blob)) + } + + err := c.JSONRPC(ctx, methodSubmitBlock, params, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +func (c *Client) GetConnections( + ctx context.Context, +) (*GetConnectionsResult, error) { + resp := &GetConnectionsResult{} + + err := c.JSONRPC(ctx, methodGetConnections, nil, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +// GetInfo retrieves general information about the state of the node and the +// network. +func (c *Client) GetInfo(ctx context.Context) (*GetInfoResult, error) { + resp := &GetInfoResult{} + + if err := c.JSONRPC(ctx, methodGetInfo, nil, resp); err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +func (c *Client) GetLastBlockHeader( + ctx context.Context, +) (*GetLastBlockHeaderResult, error) { + resp := &GetLastBlockHeaderResult{} + + err := c.JSONRPC(ctx, methodGetLastBlockHeader, nil, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +func (c *Client) GetCoinbaseTxSum( + ctx context.Context, height, count uint64, +) (*GetCoinbaseTxSumResult, error) { + resp := &GetCoinbaseTxSumResult{} + params := map[string]uint64{ + "height": height, + "count": count, + } + + err := c.JSONRPC(ctx, methodGetCoinbaseTxSum, params, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +// InnerJSON parses the content of the JSON embedded in `GetBlockResult`. +func (j *GetBlockResult) InnerJSON() (*GetBlockResultJSON, error) { + res := &GetBlockResultJSON{} + + err := json.Unmarshal([]byte(j.JSON), res) + if err != nil { + return nil, fmt.Errorf("unmarshal: %w", err) + } + + return res, nil +} + +func (c *Client) GetBlockHeadersRange( + ctx context.Context, start, end uint64, +) (*GetBlockHeadersRangeResult, error) { + resp := &GetBlockHeadersRangeResult{} + params := map[string]interface{}{ + "start_height": start, + "end_height": end, + } + + err := c.JSONRPC(ctx, methodGetBlockHeadersRange, params, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +// GetBlockHeaderByHeight retrieves block header information for either one or +// multiple blocks. +func (c *Client) GetBlockHeaderByHeight( + ctx context.Context, height uint64, +) (*GetBlockHeaderByHeightResult, error) { + resp := &GetBlockHeaderByHeightResult{} + params := map[string]interface{}{ + "height": height, + } + + err := c.JSONRPC(ctx, methodGetBlockHeaderByHeight, params, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +// GetBlockHeaderByHash retrieves block header information for either one or +// multiple blocks. +func (c *Client) GetBlockHeaderByHash( + ctx context.Context, hashes []string, +) (*GetBlockHeaderByHashResult, error) { + resp := &GetBlockHeaderByHashResult{} + params := map[string]interface{}{ + "hashes": hashes, + } + + err := c.JSONRPC(ctx, methodGetBlockHeaderByHash, params, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +// GetBlockRequestParameters represents the set of possible parameters that can +// be used for submitting a call to the `get_block` jsonrpc method. +type GetBlockRequestParameters struct { + Height uint64 `json:"height,omitempty"` + Hash string `json:"hash,omitempty"` +} + +// GetBlock fetches full block information from a block at a particular hash OR +// height. +func (c *Client) GetBlock( + ctx context.Context, params GetBlockRequestParameters, +) (*GetBlockResult, error) { + resp := &GetBlockResult{} + + err := c.JSONRPC(ctx, methodGetBlock, params, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +func (c *Client) GetFeeEstimate( + ctx context.Context, graceBlocks uint64, +) (*GetFeeEstimateResult, error) { + resp := &GetFeeEstimateResult{} + params := map[string]uint64{ + "grace_blocks": graceBlocks, + } + + err := c.JSONRPC(ctx, methodGetFeeEstimate, params, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} + +func (c *Client) SyncInfo(ctx context.Context) (*SyncInfoResult, error) { + resp := &SyncInfoResult{} + + err := c.JSONRPC(ctx, methodSyncInfo, nil, resp) + if err != nil { + return nil, fmt.Errorf("jsonrpc: %w", err) + } + + return resp, nil +} diff --git a/monero/client/rpc/daemon/raw_endpoints.go b/monero/client/rpc/daemon/raw_endpoints.go new file mode 100644 index 0000000..d1a4352 --- /dev/null +++ b/monero/client/rpc/daemon/raw_endpoints.go @@ -0,0 +1,259 @@ +package daemon + +import ( + "context" + "encoding/json" + "fmt" +) + +const ( + endpointGetHeight = "/get_height" + endpointGetLimit = "/get_limit" + endpointGetNetStats = "/get_net_stats" + endpointGetOuts = "/get_outs" + endpointGetPeerList = "/get_peer_list" + endpointGetPublicNodes = "/get_public_nodes" + endpointGetTransactionPool = "/get_transaction_pool" + endpointGetTransactionPoolStats = "/get_transaction_pool_stats" + endpointGetTransactions = "/get_transactions" + endpointMiningStatus = "/mining_status" + endpointSetLimit = "/set_limit" + endpointSetLogLevel = "/set_log_level" + endpointSetLogCategories = "/set_log_categories" + endpointStartMining = "/start_mining" + endpointStopMining = "/stop_mining" +) + +func (c *Client) StopMining( + ctx context.Context, +) (*StopMiningResult, error) { + resp := &StopMiningResult{} + + err := c.RawRequest(ctx, endpointStopMining, nil, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} + +func (c *Client) GetLimit(ctx context.Context) (*GetLimitResult, error) { + resp := &GetLimitResult{} + + err := c.RawRequest(ctx, endpointGetLimit, nil, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} + +func (c *Client) SetLogCategories( + ctx context.Context, params SetLogCategoriesRequestParameters, +) (*SetLogCategoriesResult, error) { + resp := &SetLogCategoriesResult{} + + err := c.RawRequest(ctx, endpointSetLogCategories, params, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} + +func (c *Client) SetLogLevel( + ctx context.Context, params SetLogLevelRequestParameters, +) (*SetLogLevelResult, error) { + resp := &SetLogLevelResult{} + + err := c.RawRequest(ctx, endpointSetLogLevel, params, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} + +func (c *Client) SetLimit( + ctx context.Context, params SetLimitRequestParameters, +) (*SetLimitResult, error) { + resp := &SetLimitResult{} + + err := c.RawRequest(ctx, endpointSetLimit, params, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} + +func (c *Client) StartMining( + ctx context.Context, params StartMiningRequestParameters, +) (*StartMiningResult, error) { + resp := &StartMiningResult{} + + err := c.RawRequest(ctx, endpointStartMining, params, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} + +func (c *Client) MiningStatus( + ctx context.Context, +) (*MiningStatusResult, error) { + resp := &MiningStatusResult{} + + err := c.RawRequest(ctx, endpointMiningStatus, nil, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} + +func (c *Client) GetTransactionPool( + ctx context.Context, +) (*GetTransactionPoolResult, error) { + resp := &GetTransactionPoolResult{} + + err := c.RawRequest(ctx, endpointGetTransactionPool, nil, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} + +func (c *Client) GetTransactionPoolStats( + ctx context.Context, +) (*GetTransactionPoolStatsResult, error) { + resp := &GetTransactionPoolStatsResult{} + + err := c.RawRequest(ctx, endpointGetTransactionPoolStats, nil, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} + +func (c *Client) GetPeerList( + ctx context.Context, +) (*GetPeerListResult, error) { + resp := &GetPeerListResult{} + + err := c.RawRequest(ctx, endpointGetPeerList, nil, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} + +type GetPublicNodesRequestParameters struct { + Gray bool `json:"gray"` + White bool `json:"white"` + IncludeBlocked bool `json:"include_blocked"` +} + +func (c *Client) GetPublicNodes( + ctx context.Context, params GetPublicNodesRequestParameters, +) (*GetPublicNodesResult, error) { + resp := &GetPublicNodesResult{} + + err := c.RawRequest(ctx, endpointGetPublicNodes, params, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} + +func (c *Client) GetOuts( + ctx context.Context, outputs []uint, gettxid bool, +) (*GetOutsResult, error) { + resp := &GetOutsResult{} + + type output struct { + Index uint `json:"index"` + } + + params := struct { + Outputs []output `json:"outputs"` + GetTxID bool `json:"get_txid,omitempty"` + }{GetTxID: gettxid} + + for _, out := range outputs { + params.Outputs = append(params.Outputs, output{out}) + } + + err := c.RawRequest(ctx, endpointGetOuts, params, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} + +func (c *Client) GetHeight(ctx context.Context) (*GetHeightResult, error) { + resp := &GetHeightResult{} + + err := c.RawRequest(ctx, endpointGetHeight, nil, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} + +func (c *Client) GetNetStats(ctx context.Context) (*GetNetStatsResult, error) { + resp := &GetNetStatsResult{} + + err := c.RawRequest(ctx, endpointGetNetStats, nil, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} + +func (r *GetTransactionsResult) GetTransactions() ([]*TransactionJSON, error) { + txns := make([]*TransactionJSON, len(r.Txs)) + + for idx, txn := range r.Txs { + if len(txn.AsJSON) == 0 { + return nil, fmt.Errorf("txn w/ empty `.as_json`: %s", + txn.TxHash) + } + + t := &TransactionJSON{} + err := json.Unmarshal([]byte(txn.AsJSON), t) + if err != nil { + return nil, fmt.Errorf("unmarshal txn '%s': %w", + txn.TxHash, err) + } + + txns[idx] = t + } + + return txns, nil +} + +func (c *Client) GetTransactions( + ctx context.Context, txns []string, +) (*GetTransactionsResult, error) { + resp := &GetTransactionsResult{} + params := map[string]interface{}{ + "txs_hashes": txns, + "decode_as_json": true, + } + + err := c.RawRequest(ctx, endpointGetTransactions, params, resp) + if err != nil { + return nil, fmt.Errorf("raw request: %w", err) + } + + return resp, nil +} diff --git a/monero/client/rpc/daemon/types.go b/monero/client/rpc/daemon/types.go new file mode 100644 index 0000000..721f3db --- /dev/null +++ b/monero/client/rpc/daemon/types.go @@ -0,0 +1,941 @@ +package daemon + +// RPCResultFooter contains the set of fields that every RPC result message +// will contain. +type RPCResultFooter struct { + // Status dictates whether the request worked or not. "OK" means good. + // + Status string `json:"status"` + + // States if the result is obtained using the bootstrap mode, and is + // therefore not trusted (`true`), or when the daemon is fully synced + // and thus handles the RPC locally (`false`). + // + Untrusted bool `json:"untrusted"` + + // Credits indicates the number of credits available to the requesting + // client, if payment for RPC is enabled, otherwise, 0. + // + Credits uint64 `json:"credits,omitempty"` + + // TopHash is the hash of the highest block in the chain, If payment + // for RPC is enabled, otherwise, empty. + // + TopHash string `json:"top_hash,omitempty"` +} + +// GetAlternateChainsResult is the result of a call to the GetAlternateChains +// RPC method. +type GetAlternateChainsResult struct { + // Chains is the array of alternate chains seen by the node. + // + Chains []struct { + // BlockHash is the hash of the first diverging block of this + // alternative chain. + // + BlockHash string `json:"block_hash"` + + // BlockHashes TODO + // + BlockHashes []string `json:"block_hashes"` + + // Difficulty is the cumulative difficulty of all blocks in the + // alternative chain. + // + Difficulty int64 `json:"difficulty"` + + // DifficultyTop64 is the most-significant 64 bits of the + // 128-bit network difficulty. + // + DifficultyTop64 int `json:"difficulty_top64"` + + // Height is the block height of the first diverging block of + // this alternative chain. + // + Height uint64 `json:"height"` + + // Length is the length in blocks of this alternative chain, + // after divergence. + // + Length uint64 `json:"length"` + + // MainChainParentBlock TODO + // + MainChainParentBlock string `json:"main_chain_parent_block"` + + // WideDifficulty is the network difficulty as a hexadecimal + // string representing a 128-bit number. + // + WideDifficulty string `json:"wide_difficulty"` + } `json:"chains"` + + RPCResultFooter `json:",inline"` +} + +// AccessTrackingResult is the result of a call to the RPCAccessTracking RPC +// method. +type RPCAccessTrackingResult struct { + Data []struct { + // Count is the number of times that the monero daemon received + // a request for this RPC method. + // + Count uint64 `json:"count"` + + // RPC is the name of the remote procedure call. + // + RPC string `json:"rpc"` + + // Time indicates how much time the daemon spent serving this + // procedure. + // + Time uint64 `json:"time"` + + // Credits indicates the number of credits consumed for this + // method. + // + Credits uint64 `json:"credits"` + } `json:"data"` + + RPCResultFooter `json:",inline"` +} + +// HardForkInfoResult is the result of a call to the HardForkInfo RPC method. +type HardForkInfoResult struct { + // EarliestHeight is the earliest height at which is allowed. + // + EarliestHeight int `json:"earliest_height"` + + // Whether of not the hard fork is enforced. + // + Enabled bool `json:"enabled"` + + // State indicates the current hard fork state: + // + // 0 - likely forked + // 1 - update needed + // 2 - ready + // + State int `json:"state"` + + // The number of votes required to enable . + // + Threshold int `json:"threshold"` + + // Version () corresponds to the major block version for the + // fork. + // + Version int `json:"version"` + + // Votes is the number of votes to enable + // + Votes int `json:"votes"` + + // Voting indicates which version this node is voting for/using. + // + Voting int `json:"voting"` + + // Window is the size of the voting window. + // + Window int `json:"window"` + + RPCResultFooter `json:",inline"` +} + +// GetVersionResult is the result of a call to the GetVersion RPC method. +type GetVersionResult struct { + Release bool `json:"release"` + Version uint64 `json:"version"` + + RPCResultFooter `json:",inline"` +} + +// GetBansResult is the result of a call to the GetBans RPC method. +type GetBansResult struct { + // Bans contains the list of nodes banned by this node. + // + Bans []struct { + // Host is the string representation of the node that is + // banned. + // + Host string `json:"host"` + + // IP is the integer representation of the host banned. + // + IP int `json:"ip"` + + // Seconds represents how many seconds are left for the ban to + // be lifted. + // + Seconds uint `json:"seconds"` + } `json:"bans"` + + RPCResultFooter `json:",inline"` +} + +// SetBansResult is the result of a call to the SetBans RPC method. +type SetBansResult struct { + RPCResultFooter `json:",inline"` +} + +// GetFeeEstimateResult is the result of a call to the GetFeeEstimate RPC +// method. +type GetFeeEstimateResult struct { + // Fee is the per kB fee estimate. + // + Fee int `json:"fee"` + + // QuantizationMask indicates that the fee should be rounded up to an + // even multiple of this value. + // + QuantizationMask int `json:"quantization_mask"` + + RPCResultFooter `json:",inline"` +} + +// GetInfoResult is the result of a call to the GetInfo RPC method. +type GetInfoResult struct { + AdjustedTime uint64 `json:"adjusted_time"` + AltBlocksCount int `json:"alt_blocks_count"` + BlockSizeLimit uint64 `json:"block_size_limit"` + BlockSizeMedian uint64 `json:"block_size_median"` + BlockWeightLimit uint64 `json:"block_weight_limit"` + BlockWeightMedian uint64 `json:"block_weight_median"` + BootstrapDaemonAddress string `json:"bootstrap_daemon_address"` + BusySyncing bool `json:"busy_syncing"` + CumulativeDifficulty int64 `json:"cumulative_difficulty"` + CumulativeDifficultyTop64 uint64 `json:"cumulative_difficulty_top64"` + DatabaseSize uint64 `json:"database_size"` + Difficulty uint64 `json:"difficulty"` + DifficultyTop64 uint64 `json:"difficulty_top64"` + FreeSpace uint64 `json:"free_space"` + GreyPeerlistSize uint `json:"grey_peerlist_size"` + Height uint64 `json:"height"` + HeightWithoutBootstrap uint64 `json:"height_without_bootstrap"` + IncomingConnectionsCount uint `json:"incoming_connections_count"` + Mainnet bool `json:"mainnet"` + Nettype string `json:"nettype"` + Offline bool `json:"offline"` + OutgoingConnectionsCount uint `json:"outgoing_connections_count"` + RPCConnectionsCount uint `json:"rpc_connections_count"` + Stagenet bool `json:"stagenet"` + StartTime uint64 `json:"start_time"` + Synchronized bool `json:"synchronized"` + Target uint64 `json:"target"` + TargetHeight uint64 `json:"target_height"` + Testnet bool `json:"testnet"` + TopBlockHash string `json:"top_block_hash"` + TxCount uint64 `json:"tx_count"` + TxPoolSize uint64 `json:"tx_pool_size"` + UpdateAvailable bool `json:"update_available"` + Version string `json:"version"` + WasBootstrapEverUsed bool `json:"was_bootstrap_ever_used"` + WhitePeerlistSize uint `json:"white_peerlist_size"` + WideCumulativeDifficulty string `json:"wide_cumulative_difficulty"` + WideDifficulty string `json:"wide_difficulty"` + + RPCResultFooter `json:",inline"` +} + +// GetBlockTemplateResult is the result of a call to the GetBlockTemplate RPC +// method. +type GetBlockTemplateResult struct { + // BlockhashingBlob is the blob on which to try to find a valid nonce. + // + BlockhashingBlob string `json:"blockhashing_blob"` + + // BlocktemplateBlob is the blob on which to try to mine a new block. + // + BlocktemplateBlob string `json:"blocktemplate_blob"` + + // Difficulty is the difficulty of the next block. + Difficulty int64 `json:"difficulty"` + + // ExpectedReward is the coinbase reward expected to be received if the + // block is successfully mined. + // + ExpectedReward int64 `json:"expected_reward"` + + // Height is the height on which to mine. + // + Height int `json:"height"` + + // PrevHash is the hash of the most recent block on which to mine the + // next block. + // + PrevHash string `json:"prev_hash"` + + // ReservedOffset TODO + // + ReservedOffset int `json:"reserved_offset"` + + RPCResultFooter `json:",inline"` +} + +// GetMinerDataResult is the result of a call to the GetMinerData RPC +// method. +type GetMinerDataResult struct { + MajorVersion uint8 `json:"major_version"` + Height uint64 `json:"height"` + PrevId string `json:"prev_id"` + SeedHash string `json:"seed_hash"` + Difficulty string `json:"difficulty"` + MedianWeight uint64 `json:"median_weight"` + AlreadyGeneratedCoins uint64 `json:"already_generated_coins"` + MedianTimestamp uint64 `json:"median_timestamp"` + TxBacklog []struct { + Id string `json:"id"` + BlobSize uint64 `json:"blob_size"` + Weight uint64 `json:"weight"` + Fee uint64 `json:"fee"` + } `json:"tx_backlog"` +} + +// SubmitBlockResult is the result of a call to the SubmitBlock RPC +// method. +type SubmitBlockResult struct { + Status string `json:"status"` + Error *submitBlockResultError `json:"error"` +} + +type submitBlockResultError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type Peer struct { + Host string `json:"host"` + ID uint64 `json:"id"` + IP uint32 `json:"ip"` + LastSeen int64 `json:"last_seen"` + Port uint16 `json:"port"` + PruningSeed uint32 `json:"pruning_seed"` + RPCPort uint16 `json:"rpc_port"` +} + +// GetPeerListResult is the result of a call to the GetPeerList RPC method. +type GetPeerListResult struct { + GrayList []Peer `json:"gray_list"` + WhiteList []Peer `json:"white_list"` + + RPCResultFooter `json:",inline"` +} + +// GetConnectionsResult is the result of a call to the GetConnections RPC +// method. +type GetConnectionsResult struct { + Connections []struct { + Address string `json:"address"` + AvgDownload uint64 `json:"avg_download"` + AvgUpload uint64 `json:"avg_upload"` + ConnectionID string `json:"connection_id"` + CurrentDownload uint64 `json:"current_download"` + CurrentUpload uint64 `json:"current_upload"` + Height uint64 `json:"height"` + Host string `json:"host"` + Incoming bool `json:"incoming"` + IP string `json:"ip"` + LiveTime uint64 `json:"live_time"` + LocalIP bool `json:"local_ip"` + Localhost bool `json:"localhost"` + PeerID string `json:"peer_id"` + Port string `json:"port"` + RecvCount uint64 `json:"recv_count"` + RecvIdleTime uint64 `json:"recv_idle_time"` + SendCount uint64 `json:"send_count"` + SendIdleTime uint64 `json:"send_idle_time"` + State string `json:"state"` + SupportFlags uint64 `json:"support_flags"` + } `json:"connections"` + + RPCResultFooter `json:",inline"` +} + +type GetOutsResult struct { + Outs []struct { + Height uint64 `json:"height"` + Key string `json:"key"` + Mask string `json:"mask"` + Txid string `json:"txid"` + Unlocked bool `json:"unlocked"` + } `json:"outs"` + + RPCResultFooter `json:",inline"` +} + +// GetHeightResult is the result of a call to the GetHeight RPC method. +type GetHeightResult struct { + Hash string `json:"hash"` + Height uint64 `json:"height"` + + RPCResultFooter `json:",inline"` +} + +// GetNetStatsResult is the result of a call to the GetNetStats RPC method. +type GetNetStatsResult struct { + StartTime int64 `json:"start_time"` + TotalBytesIn uint64 `json:"total_bytes_in"` + TotalBytesOut uint64 `json:"total_bytes_out"` + TotalPacketsIn uint64 `json:"total_packets_in"` + TotalPacketsOut uint64 `json:"total_packets_out"` + + RPCResultFooter `json:",inline"` +} + +// GetPublicNodesResult is the result of a call to the GetPublicNodes RPC +// method. +type GetPublicNodesResult struct { + WhiteList []Peer `json:"white"` + GrayList []Peer `json:"gray"` + + RPCResultFooter `json:",inline"` +} + +// GenerateBlocksResult is the result of a call to the GenerateBlocks RPC +// method. +type GenerateBlocksResult struct { + Blocks []string `json:"blocks"` + Height int `json:"height"` + + RPCResultFooter `json:",inline"` +} + +// GetBlockCountResult is the result of a call to the GetBlockCount RPC method. +type GetBlockCountResult struct { + Count uint64 `json:"count"` + + RPCResultFooter `json:",inline"` +} + +// RelayTxResult is the result of a call to the RelayTx RPC method. +type RelayTxResult struct { + RPCResultFooter `json:",inline"` +} + +// GetCoinbaseTxSumResult is the result of a call to the GetCoinbaseTxSum RPC +// method. +type GetCoinbaseTxSumResult struct { + EmissionAmount int64 `json:"emission_amount"` + EmissionAmountTop64 int `json:"emission_amount_top64"` + FeeAmount int `json:"fee_amount"` + FeeAmountTop64 int `json:"fee_amount_top64"` + WideEmissionAmount string `json:"wide_emission_amount"` + WideFeeAmount string `json:"wide_fee_amount"` + + RPCResultFooter `json:",inline"` +} + +type BlockHeader struct { + // BlockSize is the block size in bytes. + // + BlockSize uint64 `json:"block_size"` + + // BlockWeight TODO + // + BlockWeight uint64 `json:"block_weight"` + + // CumulativeDifficulty is the cumulative difficulty of all + // blocks up to this one. + // + CumulativeDifficulty uint64 `json:"cumulative_difficulty"` + + // CumulativeDifficultyTop64 most significant 64 bits of the + // 128-bit cumulative difficulty. + // + CumulativeDifficultyTop64 uint64 `json:"cumulative_difficulty_top64"` + + // Depth is the number of blocks succeeding this block on the + // blockchain. (the larger this number, the oldest this block + // is). + // + Depth uint64 `json:"depth"` + + // Difficulty is the difficulty that was set for mining this block. + // + Difficulty uint64 `json:"difficulty"` + + // DifficultyTop64 corresponds to the most significant 64-bit of + // the 128-bit difficulty. + // + DifficultyTop64 uint64 `json:"difficulty_top64"` + + // Hash is the hash of this block. + // + Hash string `json:"hash"` + + // Height is the number of blocks preceding this block on the + // blockchain. + // + Height uint64 `json:"height"` + + // LongTermWeight TODO + // + LongTermWeight uint64 `json:"long_term_weight"` + + // MajorVersion is the major version of the monero protocol at + // this block height. + // + MajorVersion uint `json:"major_version"` + + // MinerTxHash TODO + // + MinerTxHash string `json:"miner_tx_hash"` + + // MinorVersion is the minor version of the monero protocol at + // this block height. + // + MinorVersion uint `json:"minor_version"` + + // Nonce is the cryptographic random one-time number used in + // mining this block. + // + Nonce uint64 `json:"nonce"` + + // NumTxes is the number of transactions in this block, not + // counting the coinbase tx. + // + NumTxes uint `json:"num_txes"` + + // OrphanStatus indicates whether this block is part of the + // longest chain or not (true == not part of it). + // + OrphanStatus bool `json:"orphan_status"` + + // PowHash TODO + // + PowHash string `json:"pow_hash"` + + // PrevHash is the hash of the block immediately preceding this + // block in the chain. + // + PrevHash string `json:"prev_hash"` + + // Reward the amount of new atomic-units generated in this + // block and rewarded to the miner (1XMR = 1e12 atomic units). + // + Reward uint64 `json:"reward"` + + // Timestamp is the unix timestamp at which the block was + // recorded into the blockchain. + // + Timestamp int64 `json:"timestamp"` + + // WideCumulativeDifficulty is the cumulative difficulty of all + // blocks in the blockchain as a hexadecimal string + // representing a 128-bit number. + // + WideCumulativeDifficulty string `json:"wide_cumulative_difficulty"` + + // WideDifficulty is the network difficulty as a hexadecimal + // string representing a 128-bit number. + // + WideDifficulty string `json:"wide_difficulty"` +} + +// GetBlockResult is the result of a call to the GetBlock RPC method. +type GetBlockResult struct { + // Blob is a hexadecimal representation of the block. + // + Blob string `json:"blob"` + + // BlockHeader contains the details from the block header. + // + BlockHeader BlockHeader `json:"block_header"` + + // JSON is a json representation of the block - see + // `GetBlockResultJSON`. + // + JSON string `json:"json"` + + // MinerTxHash is the hash of the coinbase transaction + // + MinerTxHash string `json:"miner_tx_hash"` + + RPCResultFooter `json:",inline"` +} + +// GetBlockResultJSON is the internal json-formatted block information. +type GetBlockResultJSON struct { + // MajorVersion (same as in the block header) + // + MajorVersion uint `json:"major_version"` + + // MinorVersion (same as in the block header) + // + MinorVersion uint `json:"minor_version"` + + // Timestamp (same as in the block header) + // + Timestamp uint64 `json:"timestamp"` + + // PrevID (same as `block_hash` in the block header) + // + PrevID string `json:"prev_id"` + + // Nonce (same as in the block header) + // + Nonce int `json:"nonce"` + + // MinerTx contains the miner transaction information. + // + MinerTx struct { + // Version is the transaction version number + // + Version int `json:"version"` + + // UnlockTime is the block height when the coinbase transaction + // becomes spendable. + // + UnlockTime int `json:"unlock_time"` + + // Vin lists the transaction inputs. + // + Vin []struct { + Gen struct { + Height int `json:"height"` + } `json:"gen"` + } `json:"vin"` + + // Vout lists the transaction outputs. + // + Vout []struct { + Amount uint64 `json:"amount"` + Target struct { + Key string `json:"key"` + } `json:"target"` + } `json:"vout"` + // Extra (aka the transaction id) can be used to include any + // random 32byte/64char hex string. + // + Extra []int `json:"extra"` + + // RctSignatures contain the signatures of tx signers. + // + // ps.: coinbase txs DO NOT have signatures. + // + RctSignatures struct { + Type int `json:"type"` + } `json:"rct_signatures"` + } `json:"miner_tx"` + + // TxHashes is the list of hashes of non-coinbase transactions in the + // block. + // + TxHashes []string `json:"tx_hashes"` +} + +func (c *GetBlockResultJSON) MinerOutputs() uint64 { + res := uint64(0) + + for _, vout := range c.MinerTx.Vout { + res += vout.Amount + } + + return res +} + +// SyncInfoResult is the result of a call to the SyncInfo RPC method. +type SyncInfoResult struct { + Credits uint64 `json:"credits"` + + Height uint64 `json:"height"` + NextNeededPruningSeed uint64 `json:"next_needed_pruning_seed"` + Overview string `json:"overview"` + Status string `json:"status"` + TargetHeight uint64 `json:"target_height"` + TopHash string `json:"top_hash"` + Untrusted bool `json:"untrusted"` + Peers []struct { + Info struct { + Address string `json:"address"` + AddressType uint64 `json:"address_type"` + AvgDownload uint64 `json:"avg_download"` + AvgUpload uint64 `json:"avg_upload"` + ConnectionID string `json:"connection_id"` + CurrentDownload uint64 `json:"current_download"` + CurrentUpload uint64 `json:"current_upload"` + Height uint64 `json:"height"` + Host string `json:"host"` + IP string `json:"ip"` + Incoming bool `json:"incoming"` + LiveTime uint64 `json:"live_time"` + LocalIP bool `json:"local_ip"` + Localhost bool `json:"localhost"` + PeerID string `json:"peer_id"` + Port string `json:"port"` + PruningSeed uint64 `json:"pruning_seed"` + RPCCreditsPerHash uint64 `json:"rpc_credits_per_hash"` + RPCPort uint64 `json:"rpc_port"` + RecvCount uint64 `json:"recv_count"` + RecvIdleTime uint64 `json:"recv_idle_time"` + SendCount uint64 `json:"send_count"` + SendIdleTime uint64 `json:"send_idle_time"` + State string `json:"state"` + SupportFlags int `json:"support_flags"` + } `json:"info"` + } `json:"peers"` + + RPCResultFooter `json:",inline"` +} + +// GetLastBlockHeaderResult is the result of a call to the GetLastBlockHeader +// RPC method. +type GetLastBlockHeaderResult struct { + BlockHeader BlockHeader `json:"block_header"` + + RPCResultFooter `json:",inline"` +} + +// GetBlockHeadersRangeResult is the result of a call to the +// GetBlockHeadersRange RPC method. +type GetBlockHeadersRangeResult struct { + Headers []BlockHeader `json:"headers"` + + RPCResultFooter `json:",inline"` +} + +// GetBlockHeaderByHeightResult is the result of a call to the +// GetBlockHeaderByHeight RPC method. +type GetBlockHeaderByHeightResult struct { + BlockHeader BlockHeader `json:"block_header"` + + RPCResultFooter `json:",inline"` +} + +// GetBlockHeaderByHashResult is the result of a call to the +// GetBlockHeaderByHash RPC method. +type GetBlockHeaderByHashResult struct { + BlockHeader BlockHeader `json:"block_header"` + BlockHeaders []BlockHeader `json:"block_headers"` + + RPCResultFooter `json:",inline"` +} + +type MiningStatusResult struct { + Active bool `json:"active"` + Address string `json:"address"` + BgIdleThreshold int `json:"bg_idle_threshold"` + BgIgnoreBattery bool `json:"bg_ignore_battery"` + BgMinIdleSeconds uint64 `json:"bg_min_idle_seconds"` + BgTarget uint64 `json:"bg_target"` + BlockReward uint64 `json:"block_reward"` + BlockTarget uint64 `json:"block_target"` + Difficulty uint64 `json:"difficulty"` + DifficultyTop64 uint64 `json:"difficulty_top64"` + IsBackgroundMiningEnabled bool `json:"is_background_mining_enabled"` + PowAlgorithm string `json:"pow_algorithm"` + Speed uint64 `json:"speed"` + ThreadsCount uint64 `json:"threads_count"` + WideDifficulty string `json:"wide_difficulty"` + + RPCResultFooter `json:",inline"` +} + +// GetTransactionPoolStatsResult is the result of a call to the +// GetTransactionPoolStats RPC method. +type GetTransactionPoolStatsResult struct { + PoolStats struct { + BytesMax uint64 `json:"bytes_max"` + BytesMed uint64 `json:"bytes_med"` + BytesMin uint64 `json:"bytes_min"` + BytesTotal uint64 `json:"bytes_total"` + FeeTotal uint64 `json:"fee_total"` + Histo []struct { + Bytes uint64 `json:"bytes"` + Txs uint64 `json:"txs"` + } `json:"histo"` + Histo98Pc uint64 `json:"histo_98pc"` + Num10M uint64 `json:"num_10m"` + NumDoubleSpends uint64 `json:"num_double_spends"` + NumFailing uint64 `json:"num_failing"` + NumNotRelayed uint64 `json:"num_not_relayed"` + Oldest int64 `json:"oldest"` + TxsTotal uint64 `json:"txs_total"` + } `json:"pool_stats"` + + RPCResultFooter `json:",inline"` +} + +type GetTransactionsResultTransaction struct { + AsHex string `json:"as_hex"` + AsJSON string `json:"as_json"` + BlockHeight uint64 `json:"block_height"` + BlockTimestamp int64 `json:"block_timestamp"` + DoubleSpendSeen bool `json:"double_spend_seen"` + InPool bool `json:"in_pool"` + OutputIndices []int `json:"output_indices"` + PrunableAsHex string `json:"prunable_as_hex"` + PrunableHash string `json:"prunable_hash"` + PrunedAsHex string `json:"pruned_as_hex"` + TxHash string `json:"tx_hash"` +} + +type GetTransactionsResult struct { + Credits int `json:"credits"` + Status string `json:"status"` + TopHash string `json:"top_hash"` + Txs []GetTransactionsResultTransaction `json:"txs"` + TxsAsHex []string `json:"txs_as_hex"` + Untrusted bool `json:"untrusted"` +} + +type TransactionJSON struct { + Version int `json:"version"` + UnlockTime int `json:"unlock_time"` + Vin []struct { + Key struct { + Amount int `json:"amount"` + KeyOffsets []uint `json:"key_offsets"` + KImage string `json:"k_image"` + } `json:"key"` + } `json:"vin"` + Vout []struct { + Amount uint64 `json:"amount"` + Target struct { + Key string `json:"key"` + } `json:"target"` + } `json:"vout"` + Extra []byte `json:"extra"` + RctSignatures struct { + Type int `json:"type"` + Txnfee uint64 `json:"txnFee"` + Ecdhinfo []struct { + Amount string `json:"amount"` + } `json:"ecdhInfo"` + Outpk []string `json:"outPk"` + } `json:"rct_signatures"` + RctsigPrunable struct { + Nbp int `json:"nbp"` + Bp []struct { + A string `json:"A"` + S string `json:"S"` + T1 string `json:"T1"` + T2 string `json:"T2"` + Taux string `json:"taux"` + Mu string `json:"mu"` + L []string `json:"L"` + R []string `json:"R"` + LowerA string `json:"a"` + B string `json:"b"` + T string `json:"t"` + } `json:"bp,omitempty"` + Bpp []struct { + A string `json:"A"` + A1 string `json:"A1"` + B string `json:"B"` + R1 string `json:"r1"` + S1 string `json:"s1"` + D1 string `json:"d1"` + L []string `json:"L"` + R []string `json:"R"` + } `json:"bpp,omitempty"` + Clsags []struct { + S []string `json:"s"` + C1 string `json:"c1"` + D string `json:"D"` + } `json:"CLSAGs,omitempty"` + Pseudoouts []string `json:"pseudoOuts"` + } `json:"rctsig_prunable"` +} + +type GetTransactionPoolResult struct { + Credits int `json:"credits"` + SpentKeyImages []struct { + IDHash string `json:"id_hash"` + TxsHashes []string `json:"txs_hashes"` + } `json:"spent_key_images"` + Status string `json:"status"` + TopHash string `json:"top_hash"` + Transactions []struct { + BlobSize uint64 `json:"blob_size"` + DoNotRelay bool `json:"do_not_relay"` + DoubleSpendSeen bool `json:"double_spend_seen"` + Fee uint64 `json:"fee"` + IDHash string `json:"id_hash"` + KeptByBlock bool `json:"kept_by_block"` + LastFailedHeight uint64 `json:"last_failed_height"` + LastFailedIDHash string `json:"last_failed_id_hash"` + LastRelayedTime uint64 `json:"last_relayed_time"` + MaxUsedBlockHeight uint64 `json:"max_used_block_height"` + MaxUsedBlockIDHash string `json:"max_used_block_id_hash"` + ReceiveTime int64 `json:"receive_time"` + Relayed bool `json:"relayed"` + TxBlob string `json:"tx_blob"` + TxJSON string `json:"tx_json"` + Weight uint64 `json:"weight"` + } `json:"transactions"` + Untrusted bool `json:"untrusted"` +} + +type SetLogCategoriesRequestParameters struct { + // Categories to log with their corresponding levels formatted as a + // comma-separated list of : pairs. + // + // For instance, to activate verbosity 1 for the `net.http` category + // and verbosity 4 for `net.dns`: + // + // net.htpp:1,net.dns:4 + // + Categories string `json:"categories"` +} + +type SetLogCategoriesResult struct { + Categories string `json:"categories"` + RPCResultFooter `json:",inline"` +} + +type SetLogLevelRequestParameters struct { + // Level is the log level that the daemon should use. From 0 to 4 (less + // verbose to more verbose). + // + Level int8 `json:"level"` +} + +type SetLogLevelResult struct { + RPCResultFooter `json:",inline"` +} + +type SetLimitRequestParameters struct { + // LimitUp is the upload limit in kB/s + // + LimitUp uint64 `json:"limit_up"` + // LimitDown is the download limit in kB/s + // + LimitDown uint64 `json:"limit_down"` +} + +type SetLimitResult struct { + // LimitUp is the upload limit in kB/s + // + LimitUp uint64 `json:"limit_up"` + // LimitDOwn is the download limit in kB/s + // + LimitDown uint64 `json:"limit_down"` + + RPCResultFooter `json:",inline"` +} + +type GetLimitResult struct { + // LimitUp is the upload limit in kB/s + // + LimitUp uint64 `json:"limit_up"` + // LimitDown is the download limit in kB/s + // + LimitDown uint64 `json:"limit_down"` + + RPCResultFooter `json:",inline"` +} + +type StartMiningRequestParameters struct { + MinerAddress string `json:"miner_address"` + BackgroundMining bool `json:"background_mining"` + IgnoreBattery bool `json:"ignore_battery"` + ThreadsCount uint `json:"threads_count"` +} + +type StartMiningResult struct { + RPCResultFooter `json:",inline"` +} + +type StopMiningResult struct { + RPCResultFooter `json:",inline"` +} diff --git a/monero/client/rpc/doc.go b/monero/client/rpc/doc.go new file mode 100644 index 0000000..180818b --- /dev/null +++ b/monero/client/rpc/doc.go @@ -0,0 +1,4 @@ +// Package rpc provides a client that's able to communicate with a `monerod` +// daemon via its RPC interfaces. +// +package rpc diff --git a/monero/client/tx.go b/monero/client/tx.go index fe1000a..dd73b7c 100644 --- a/monero/client/tx.go +++ b/monero/client/tx.go @@ -1,9 +1,9 @@ package client import ( + "git.gammaspectra.live/P2Pool/consensus/v3/monero/client/rpc/daemon" "git.gammaspectra.live/P2Pool/consensus/v3/p2pool/mempool" "git.gammaspectra.live/P2Pool/consensus/v3/types" - "git.gammaspectra.live/P2Pool/go-monero/pkg/rpc/daemon" ) func isRctBulletproof(t int) bool {