├── .github └── workflows │ ├── examples-console.yml │ └── sdk.yml ├── .gitignore ├── Getting Started.md ├── LICENSE ├── README-diagram.png ├── README.md ├── benchmarks ├── Benchmark Report 2021-10-25.md └── Benchmark Report 2021-11-05.md ├── examples ├── console │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── snapshot.go │ ├── stats.go │ └── util.go └── dashboard.html ├── go.work ├── go.work.sum ├── sdk ├── .golangci.yml ├── agent │ ├── agent.go │ ├── agent_test.go │ ├── agenthttp │ │ └── agenthttp.go │ ├── bufferedagent │ │ ├── agent.go │ │ ├── buffered_memo.go │ │ ├── buffered_payment.go │ │ ├── events.go │ │ └── tcp.go │ ├── events.go │ ├── hash.go │ ├── horizon │ │ ├── balance_collector.go │ │ ├── doc.go │ │ ├── sequence_number_collector.go │ │ ├── streamer.go │ │ ├── streamer_test.go │ │ └── submitter.go │ ├── ingest.go │ ├── msg │ │ └── msg.go │ ├── submit │ │ └── submitter.go │ └── tcp.go ├── go.mod ├── go.sum ├── state │ ├── asset.go │ ├── asset_test.go │ ├── close.go │ ├── close_test.go │ ├── doc.go │ ├── hash.go │ ├── hash_test.go │ ├── ingest.go │ ├── ingest_test.go │ ├── integrationtests │ │ ├── helpers_test.go │ │ ├── main_test.go │ │ └── state_test.go │ ├── open.go │ ├── open_test.go │ ├── payment.go │ ├── payment_test.go │ ├── state.go │ ├── state_test.go │ └── verify.go └── txbuild │ ├── close.go │ ├── close_test.go │ ├── create_channelaccount.go │ ├── declaration.go │ ├── declaration_test.go │ ├── doc.go │ ├── open.go │ ├── sequence.go │ ├── sequence_test.go │ └── txbuildtest │ ├── txbuildtest.go │ └── txbuildtest_test.go └── specifications ├── diagrams ├── README.md ├── sequence-diagrams │ ├── setup-payment-close.txt │ └── withdraw.txt └── state-diagrams │ ├── channel-proposal.txt │ ├── channel-state.txt │ └── payment-proposal-threads.txt └── starlight-payment-channel-mechanics.md /.github/workflows/examples-console.yml: -------------------------------------------------------------------------------- 1 | name: 'Examples: Console' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@5c56cd6c9dc07901af25baab6f2b0d9f3b7c3018 22 | with: 23 | version: v1.45.2 24 | working-directory: examples/console 25 | skip-go-installation: true 26 | 27 | build: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v2 32 | - name: Set up Go 33 | uses: actions/setup-go@v2 34 | with: 35 | go-version: 1 36 | - name: Build 37 | working-directory: examples/console 38 | run: go build ./... 39 | - name: Build Tests 40 | working-directory: examples/console 41 | run: go test -exec=echo ./... 42 | -------------------------------------------------------------------------------- /.github/workflows/sdk.yml: -------------------------------------------------------------------------------- 1 | name: SDK 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@5c56cd6c9dc07901af25baab6f2b0d9f3b7c3018 22 | with: 23 | version: v1.45.2 24 | working-directory: sdk 25 | skip-go-installation: true 26 | 27 | build: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v2 32 | - name: Set up Go 33 | uses: actions/setup-go@v2 34 | with: 35 | go-version: 1 36 | - name: Build SDK 37 | working-directory: sdk 38 | run: go build ./... 39 | - name: Build SDK Tests 40 | working-directory: sdk 41 | run: go test -exec=echo ./... 42 | 43 | unit-tests: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v2 48 | - name: Set up Go 49 | uses: actions/setup-go@v2 50 | with: 51 | go-version: 1 52 | - name: Run SDK Unit Tests 53 | working-directory: sdk 54 | run: go test -v -race ./... 55 | 56 | integration-tests: 57 | runs-on: ubuntu-latest 58 | steps: 59 | - name: "Stellar Quickstart: Run" 60 | run: docker run -d -p 8000:8000 --name stellar stellar/quickstart --standalone --enable-core-artificially-accelerate-time-for-testing 61 | - name: "Stellar Quickstart: Wait for Ready" 62 | run: while ! [ "$(curl -s --fail localhost:8000 | jq '.history_latest_ledger')" -gt 0 ]; do echo waiting; sleep 1; done 63 | - name: "Stellar Quickstart: Details" 64 | run: curl localhost:8000 65 | - name: Checkout 66 | uses: actions/checkout@v2 67 | - name: Set up Go 68 | uses: actions/setup-go@v2 69 | with: 70 | go-version: 1 71 | - name: Run SDK Integration Tests 72 | working-directory: sdk 73 | run: go test -v -race -p=1 ./**/integrationtests 74 | env: 75 | INTEGRATION_TESTS: 1 76 | INTEGRATION_TESTS_HORIZON_URL: http://localhost:8000 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.test 3 | *.prof 4 | -------------------------------------------------------------------------------- /Getting Started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | - [Testing with testnet](#testing-with-testnet) 4 | - [Running your own devnet with CAP-21 and CAP-40](#running-your-own-devnet-with-cap-21-and-cap-40) 5 | - [Experiment with the prototype Starlight Go SDK](#experiment-with-the-prototype-starlight-go-sdk) 6 | - [Run the console example application](#run-the-console-example-application) 7 | - [Manually inspect or build transactions](#manually-inspect-or-build-transactions) 8 | 9 | ## Testing with testnet 10 | 11 | The Stellar testnet has protocol 19 implemented: 12 | 13 | https://horizon-testnet.stellar.org 14 | 15 | ## Running your own devnet with CAP-21 and CAP-40 16 | 17 | To run a standalone network use the branch of the `stellar/quickstart` docker image: 18 | 19 | ### Locally 20 | 21 | ``` 22 | docker run --rm -it -p 8000:8000 --name stellar stellar/quickstart:testing --standalone 23 | ``` 24 | 25 | Test that the network is running by curling the root endpoint: 26 | 27 | ``` 28 | curl http://localhost:8000 29 | ``` 30 | 31 | There is a friendbot included in the network that can faucet new accounts. 32 | 33 | ### Deployed 34 | 35 | Alternatively, deploy a standalone network to Digital Ocean to develop and test with: 36 | 37 | [![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/stellar/docker-stellar-core-horizon/tree/protocol19) 38 | 39 | The network passphrase will be randomly generated in this case, and you can retrieve it from the logs. 40 | 41 | _Reminder:_ The DigitalOcean server is publicly accessible on the Internet. Do not put sensitive information on the network that you would not want someone else to know. Anyone with access to the network will be able to use the root account above. 42 | 43 | ## Experiment with the prototype Starlight Go SDK 44 | 45 | The `sdk` directory contains the prototype Starlight Go SDK. 46 | 47 | Reference: https://pkg.go.dev/github.com/stellar/starlight/sdk 48 | 49 | ``` 50 | go get github.com/stellar/starlight/sdk 51 | ``` 52 | 53 | See the [console example](https://github.com/stellar/experimental-payment-channels/tree/readme/examples/console) application for an example for how to use the Starlight agent. 54 | 55 | ## Run the console example application 56 | 57 | The `examples/console` directory contains an example application that operates a payment channel between two participants over a TCP connection. Requires a network to be running with support for CAP-21 and CAP-40. 58 | 59 | See the [README](examples/console/README.md) for more details. 60 | 61 | ## Manually inspect or build transactions 62 | 63 | You can use [stc](https://github.com/xdrpp/stc) to manually build and inspect transactions at the command line using text files. 64 | 65 | Build transactions as you normally would, but for standalone network. 66 | ``` 67 | stc -net=standalone -edit tx 68 | stc -net=standalone -sign tx | stc -net=standalone -post - 69 | ``` 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stellar-deprecated/starlight/919096552e44eec5ec3fd6278922396926239874/README-diagram.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Stellar 3 |
4 | Creating equitable access to the global financial system 5 |

Starlight Protocol

6 |
7 |

8 | 9 | Go Reference 10 | Discussions 11 |

12 | 13 | Starlight is a prototype layer 2 payment channel protocol for the Stellar Network. Starlight has existed in a couple different forms. The previous version of Starlight lives at [interstellar/starlight](https://github.com/interstellar/starlight). 14 | 15 | **Have a use case for payment channels on Stellar? Tell us about it [here](https://github.com/stellar/starlight/discussions/new?category=use-cases).** 16 | 17 | This repository contains a experiments, prototypes, documents, and issues 18 | relating to implementing the Starlight protocol on the Stellar network. 19 | Protoypes here are dependent on Protocol 19 and Core Advancement Protocols, 20 | [CAP-21] and [CAP-40]. You can experiment with the Starlight protocol by using 21 | the testnet, or running a Stellar network in a docker container. To find out 22 | how, see [Getting Started](Getting%20Started.md). 23 | 24 | ![Diagram of two people opening a payment channel, transacting off-network, and closing the payment channel.](README-diagram.png) 25 | 26 | The Starlight protocol, SDK, code in this repository, and any forks of other Stellar software referenced here, are **experimental** and **not recommended for use in production** systems. Please use the SDK to experiment with payment channels on Stellar, but it is not recommended for use with assets that hold real world value. 27 | 28 | ## Try it out 29 | 30 | Run the example console application with testnet: 31 | 32 | ``` 33 | git clone https://github.com/stellar/starlight 34 | cd examples/console 35 | go run . 36 | >>> help 37 | ``` 38 | 39 | Run two copies of the example console application and connect them directly over 40 | TCP to open a payment channel between two participants. 41 | More details in the [README](https://github.com/stellar/starlight/tree/main/examples/console). 42 | 43 | ## Get involved 44 | 45 | - [Discord](https://discord.gg/xGWRjyNzQh) 46 | - [Discussions](https://github.com/stellar/starlight/discussions) 47 | - [Demos](https://github.com/stellar/starlight/discussions/categories/demos) 48 | - [Getting Started](Getting%20Started.md) 49 | - [Specifications](specifications/) 50 | - [Benchmarks](benchmarks/) 51 | 52 | ## Build and experiment 53 | 54 | - [SDK](https://pkg.go.dev/github.com/stellar/starlight/sdk) 55 | - [Examples](examples/) 56 | 57 | ## Discussions 58 | 59 | - Live chat is on the `#starlight` channel in [Discord](https://discord.gg/xGWRjyNzQh) 60 | - Discussions about Starlight are on [GitHub Discussions](https://github.com/stellar/starlight/discussions) 61 | 62 | ## Protocol 19 63 | 64 | The code in this repository is dependent on changes to the Stellar protocol coming in Protocol 19, specifically the changes described by [CAP-21] and [CAP-40]. Protocol 19 is released. 65 | 66 | [CAP-21]: https://stellar.org/protocol/cap-21 67 | [CAP-40]: https://stellar.org/protocol/cap-40 68 | -------------------------------------------------------------------------------- /benchmarks/Benchmark Report 2021-10-25.md: -------------------------------------------------------------------------------- 1 | # Benchmark Report 2021-10-25 2 | 3 | This report is a summary of the results seen on 2021-10-25 and 2021-10-26 4 | benchmarking a Starlight payment channel. This report details the setup and how 5 | the benchmark was run, as well as the results. 6 | 7 | The application used for the benchmarking is the `examples/console` application 8 | in this repository. Some aspects of the application have been optimized, but 9 | many parts have not. As an example, participants use simple JSON where some of 10 | the message is compressed. The results in this report should not be viewed as 11 | the maximum possible performance possible with a Starlight payment channel. 12 | 13 | ## Summary 14 | 15 | The test demonstrated that performance is influenced significantly by the 16 | latency of the network connection between the payment channel participants. 17 | However, even connections with greater latency, such as those spanning 18 | countries, can deliver high numbers of payments per second. 19 | 20 | The tests use a Starlight bi-directional payment channel, but only move assets 21 | in a single direction for the duration of the test. The tests send payments 22 | using buffering, where payments are buffered and sent in batches. 23 | 24 | Test AB6 demonstrated over 400,000 payments per second for payments varying from 25 | 0.0000001 to 1000.0, over Internet with 10-20ms latency. 26 | 27 | Since smaller payment amounts take up less data, higher payments per second were 28 | observed for payments that trend towards micro-payments. 29 | 30 | For example, test AB5 demonstrated over 700,000 payments per second for payments 31 | varying from 0.0000001 to 1.0. And, test AB2 over 1,190,000 payments per second 32 | with micro-payments where every payment is for 0.0000001. 33 | 34 | Tests AC1, AC2, AC3 also demonstrated that at least 17,000 to 130,000 payments 35 | per second is possible on networks with latency as high as 215ms. 36 | 37 | ## Participants 38 | 39 | Three participants were involved in the tests, with a payment channel formed 40 | between participants A and B, and also between participants A and C. 41 | 42 | ### Participant A 43 | 44 | - Location: San Francisco, US 45 | - Network: 1gbps 46 | - Hardware: 47 | ``` 48 | MacBook Pro (13-inch, 2019, Four Thunderbolt 3 ports) 49 | 2.8 GHz Quad-Core Intel Core i7 50 | 16 GB 2133 MHz LPDDR3 51 | ``` 52 | 53 | ### Participant B 54 | 55 | - Location: San Francisco, US 56 | - Network: ~200mbps 57 | - Hardware: 58 | ``` 59 | MacBook Pro (16-inch, 2019) 60 | 2.6 GHz 6-Core Intel Core i7 61 | 32 GB 2667 MHz DDR4 62 | ``` 63 | 64 | ### Participant C 65 | 66 | - Location: Florianopolis, Brazil 67 | - Network: 200mbps 68 | - Hardware: 69 | ``` 70 | MacBook Air (M1, 2020) 71 | Apple M1 72 | 16 GB 73 | ``` 74 | 75 | ## Setup 76 | 77 | A preivate network running using Docker container from: 78 | https://github.com/stellar/docker-stellar-core-horizon/tree/4d34da83d0876e21ff68cf90653053879239707b 79 | 80 | Benchmark application in use `examples/console` from: 81 | https://github.com/stellar/starlight/tree/f43363c6b1593134344629b32883529f494bab23 82 | 83 | Note in both setups below no snapshots of the channel state are being written to 84 | disk. Data persistence functionality exists in the console application and can 85 | be enabled, but was not enabled for these tests. However, no significant 86 | difference in performance has been witnessed with having that functionality 87 | enabled. 88 | 89 | ### Listener 90 | 91 | ``` 92 | $ cd examples/console 93 | $ go run -horizon= -listen=:8001 94 | ``` 95 | 96 | ### Connector 97 | 98 | ``` 99 | $ cd examples/console 100 | $ go run -horizon= -http=9000 -connect=:8001 101 | > open USD 102 | > deposit 10000000 103 | > deposit 10000000 other 104 | ``` 105 | 106 | ## Tests and Results 107 | 108 | ### Understanding the Tests 109 | 110 | Each test sends payments in one direction on the bi-directional payment channel. 111 | Each test specifies a max payment amount, payment count, and a maximum buffer 112 | size in the `payx` command. i.e. `payx 113 | `. 114 | 115 | For example, test AB6 `payx 1000 1000000 50000`, sent 1 million payments, with 116 | amounts varying from 0.0000001 to 1000.0, and it allowed payments to buffer into 117 | batches holding up to 50k payments. 118 | 119 | Payments are buffered until the channel is available to propose the next 120 | agreement. An agreement is then built containing one or more buffered payments. 121 | The agreement containing a list of all buffered payments are transmitted to the 122 | recipient, and back again in full. 123 | 124 | Agreements TPS is the number of agreements per second that the participants have 125 | signed and exchanged. Agreements are transmitted across the wire and so the 126 | agreements TPS is limited by the patency of both participants and their 127 | capability to quickly produce ed25519 signatures for the two transactions that 128 | form each agreement. Agreements TPS is limited by the latency between both 129 | participants. For example, if the participants have a ping time of 30ms, the 130 | highest agreements TPS value possible will be 1s/30ms=33tps. Or if the 131 | participants have a ping time of 200ms, the highest agreements TPS value 132 | possible will be 1s/200ms=5tps. 133 | 134 | Buffered payments TPS is the number of buffered payments per second that the 135 | participants have exchanged inside agreements. Buffered payments are transmitted 136 | across the wire inside an agreement. The maximum buffer size is the maximum 137 | number of payments that will be transmitted in a single agreement. Buffered 138 | payments TPS are limited by the bandwidth and latency between both participants. 139 | 140 | Buffered payments average buffer size is the average size in bytes of the 141 | buffers that were transmitted between participants. 142 | 143 | For more information on buffering payments, see this discussion: https://github.com/stellar/starlight/discussions/330#discussioncomment-1345257 144 | 145 | #### Participants A (US) and B (US) 146 | 147 | Latency between participants A and B was observed to be approximately 10-30ms. 148 | 149 | ##### Test AB1 150 | 151 | ``` 152 | > payx 0.0000001 3000000 15000 153 | sending 0.0000001 payment 3000000 times 154 | time spent: 4.063101492s 155 | agreements sent: 201 156 | agreements received: 0 157 | agreements tps: 49.470 158 | buffered payments sent: 3000000 159 | buffered payments received: 0 160 | buffered payments tps: 738352.218 161 | buffered payments max buffer size: 55072 162 | buffered payments min buffer size: 600 163 | buffered payments avg buffer size: 48020 164 | ``` 165 | 166 | ##### Test AB2 167 | 168 | ``` 169 | > payx 0.0000001 3000000 95000 170 | sending 0.0000001 payment 3000000 times 171 | time spent: 2.50396729s 172 | agreements sent: 33 173 | agreements received: 0 174 | agreements tps: 13.179 175 | buffered payments sent: 3000000 176 | buffered payments received: 0 177 | buffered payments tps: 1198098.718 178 | buffered payments max buffer size: 349356 179 | buffered payments min buffer size: 13380 180 | buffered payments avg buffer size: 234642 181 | ``` 182 | 183 | ##### Test AB3 184 | 185 | ``` 186 | > payx 1 3000000 15000 187 | sending 1 payment 3000000 times 188 | time spent: 5.898862686s 189 | agreements sent: 201 190 | agreements received: 0 191 | agreements tps: 34.074 192 | buffered payments sent: 3000000 193 | buffered payments received: 0 194 | buffered payments tps: 508572.611 195 | buffered payments max buffer size: 153684 196 | buffered payments min buffer size: 1980 197 | buffered payments avg buffer size: 152424 198 | ``` 199 | 200 | ##### Test AB4 201 | 202 | ``` 203 | > payx 1 3000000 1000 204 | sending 1 payment 3000000 times 205 | time spent: 39.129140064s 206 | agreements sent: 3001 207 | agreements received: 0 208 | agreements tps: 76.695 209 | buffered payments sent: 3000000 210 | buffered payments received: 0 211 | buffered payments tps: 76669.203 212 | buffered payments max buffer size: 10292 213 | buffered payments min buffer size: 1140 214 | buffered payments avg buffer size: 9601 215 | ``` 216 | 217 | ##### Test AB5 218 | 219 | ``` 220 | > payx 1 1000000 50000 221 | sending 1 payment 1000000 times 222 | time spent: 1.427511708s 223 | agreements sent: 21 224 | agreements received: 0 225 | agreements tps: 14.711 226 | buffered payments sent: 1000000 227 | buffered payments received: 0 228 | buffered payments tps: 700519.649 229 | buffered payments max buffer size: 503908 230 | buffered payments min buffer size: 1992 231 | buffered payments avg buffer size: 502620 232 | ``` 233 | 234 | ##### Test AB6 235 | 236 | ``` 237 | > payx 1000 1000000 50000 238 | sending 1000 payment 1000000 times 239 | time spent: 2.421638555s 240 | agreements sent: 21 241 | agreements received: 0 242 | agreements tps: 8.672 243 | buffered payments sent: 1000000 244 | buffered payments received: 0 245 | buffered payments tps: 412943.541 246 | buffered payments max buffer size: 619884 247 | buffered payments min buffer size: 138768 248 | buffered payments avg buffer size: 549220 249 | ``` 250 | 251 | #### Participants A (US) and C (Brazil) 252 | 253 | Latency between participants A and C was observed to be approximately 215ms. 254 | 255 | ##### Test AC1 256 | 257 | ``` 258 | > payx 0.0000001 3000000 15000 259 | sending 0.0000001 payment 3000000 times 260 | time spent: 1m28.168218631s 261 | agreements sent: 201 262 | agreements received: 0 263 | agreements tps: 2.280 264 | buffered payments sent: 3000000 265 | buffered payments received: 0 266 | buffered payments tps: 34025.866 267 | buffered payments max buffer size: 55144 268 | buffered payments min buffer size: 1520 269 | buffered payments avg buffer size: 47475 270 | ``` 271 | 272 | ##### Test AC2 273 | 274 | ``` 275 | > payx 0.0000001 3000000 95000 276 | sending 0.0000001 payment 3000000 times 277 | time spent: 22.827299992s 278 | agreements sent: 33 279 | agreements received: 0 280 | agreements tps: 1.446 281 | buffered payments sent: 3000000 282 | buffered payments received: 0 283 | buffered payments tps: 131421.587 284 | buffered payments max buffer size: 349288 285 | buffered payments min buffer size: 952 286 | buffered payments avg buffer size: 240436 287 | ``` 288 | 289 | ##### Test AC3 290 | 291 | ``` 292 | > payx 1 3000000 15000 293 | sending 1 payment 3000000 times 294 | time spent: 2m47.082713s 295 | agreements sent: 201 296 | agreements received: 0 297 | agreements tps: 1.203 298 | buffered payments sent: 3000000 299 | buffered payments received: 0 300 | buffered payments tps: 17955.179 301 | buffered payments max buffer size: 153592 302 | buffered payments min buffer size: 1648 303 | buffered payments avg buffer size: 152538 304 | ``` 305 | 306 | ##### Test AC4 307 | 308 | ``` 309 | > payx 1 3000000 1000 310 | sending 1 payment 3000000 times 311 | time spent: 12m37.733772304s 312 | agreements sent: 3001 313 | agreements received: 0 314 | agreements tps: 3.960 315 | buffered payments sent: 3000000 316 | buffered payments received: 0 317 | buffered payments tps: 3959.174 318 | buffered payments max buffer size: 10484 319 | buffered payments min buffer size: 3376 320 | buffered payments avg buffer size: 6816 321 | ``` 322 | -------------------------------------------------------------------------------- /benchmarks/Benchmark Report 2021-11-05.md: -------------------------------------------------------------------------------- 1 | # Benchmark Report 2021-11-05 2 | 3 | This report is a summary of the results seen on 2021-11-05 benchmarking a 4 | Starlight payment channel. This report details the setup and how the benchmark 5 | was run, as well as the results. 6 | 7 | The application used for the benchmarking is the `examples/console` application 8 | in this repository. Some aspects of the application have been optimized, but 9 | many parts have not. The results in this report should not be viewed as the 10 | maximum possible performance possible with a Starlight payment channel. 11 | 12 | ## Summary 13 | 14 | The tests use a Starlight bi-directional payment channel, but only move assets 15 | in a single direction for the duration of the test. The tests send payments 16 | using buffering, where payments are buffered and sent in batches. 17 | 18 | Test AB1 enabled snapshotting and demonstrated 1.1 million payments per second 19 | for payments varying from 0.0000001 to 0.001. 20 | 21 | Test AB2 disabled snapshotting and demonstrated similar, over 1.5 million 22 | payments per second for payments varying from 0.0000001 to 0.001. 23 | 24 | Test AB3 demonstrated over 1.5 million payments per second for payments varying 25 | from 1 to 1000.0, over Internet with 20-30ms latency. 26 | 27 | Test AB4 sent payments serially and was therefore limited by the network latency 28 | between participants and demonstrated 38 payments and agreements per second. 29 | 30 | An impact on performance was witnessed when enabling snapshotting as both 31 | participants wrote the entire state of the channel to a JSON file. The 32 | application writes the file and blocks until that is complete to ensure the 33 | state of the channel is persisted before continuing and this briefly delays the 34 | sending of the next payment. 35 | 36 | There was no noticeable difference in buffer size and performance with different 37 | size payments during these tests even though smaller amounts do result in 38 | smaller buffers before they're compressed. This is because amounts are encoded 39 | as variable binary integers into the minimum number of bytes required to hold 40 | the value. The lack of difference in the final buffer size may be due to GZIP 41 | compression that is used to reduce the size of the buffers. 42 | 43 | ## Participants 44 | 45 | Two participants were involved in the tests, with a payment channel formed 46 | between participants A and B. 47 | 48 | ### Participant A 49 | 50 | - Location: San Francisco, US 51 | - Network: 1gbps 52 | - Hardware: 53 | ``` 54 | MacBook Pro (13-inch, 2019, Four Thunderbolt 3 ports) 55 | 2.8 GHz Quad-Core Intel Core i7 56 | 16 GB 2133 MHz LPDDR3 57 | ``` 58 | 59 | ### Participant B 60 | 61 | - Location: San Francisco, US 62 | - Network: ~200mbps 63 | - Hardware: 64 | ``` 65 | MacBook Pro (16-inch, 2019) 66 | 2.6 GHz 6-Core Intel Core i7 67 | 32 GB 2667 MHz DDR4 68 | ``` 69 | 70 | ## Setup 71 | 72 | A private network running using Docker container from: 73 | https://github.com/stellar/docker-stellar-core-horizon/tree/4d34da83d0876e21ff68cf90653053879239707b 74 | 75 | Benchmark application in use `examples/console` from: 76 | https://github.com/stellar/starlight/tree/9dbede9133883d6fcd69ca037c3e3466bad1f974 77 | 78 | Note in some tests no snapshots of the channel state are being written to disk. 79 | See each test for whether it was enabled or not. Tests that had it enabled used 80 | the `-f` option included as optional below. 81 | 82 | ### Listener 83 | 84 | ``` 85 | $ cd examples/console 86 | $ go run -horizon= -listen=:8001 [-f snapshot.json] 87 | ``` 88 | 89 | ### Connector 90 | 91 | ``` 92 | $ cd examples/console 93 | $ go run -horizon= -connect=:8001 [-f snapshot.json] 94 | > open USD 95 | > deposit 90000000000 96 | ``` 97 | 98 | ## Tests and Results 99 | 100 | ### Understanding the Tests 101 | 102 | Each test sends payments in one direction on the bi-directional payment channel. 103 | Each test specifies a max payment amount, payment count, and a maximum buffer 104 | size in the `payx` command. i.e. `payx 105 | `. 106 | 107 | For example, test AB1 `payx 0.001 10000000 95000`, sent 1 million payments, with 108 | amounts varying from 0.00000001 to 0.001, and it allowed payments to buffer into 109 | batches holding up to 95k payments. 110 | 111 | Payments are buffered until the channel is available to propose the next 112 | agreement. An agreement is then built containing one or more buffered payments. 113 | The agreement containing a list of all buffered payments are transmitted to the 114 | recipient, and back again in full. 115 | 116 | Agreements TPS is the number of agreements per second that the participants have 117 | signed and exchanged. Agreements are transmitted across the wire and so the 118 | agreements TPS is limited by the latency between both participants and their 119 | capability to quickly produce ed25519 signatures for the two transactions that 120 | form each agreement. For example, if the participants have a ping time of 30ms, 121 | the highest agreements TPS value possible will be 1s/30ms=33tps. 122 | 123 | Buffered payments TPS is the number of buffered payments per second that the 124 | participants have exchanged inside agreements. Buffered payments are transmitted 125 | across the wire inside an agreement. The maximum buffer size is the maximum 126 | number of payments that will be transmitted in a single agreement. Buffered 127 | payments TPS are limited by the bandwidth and latency between both participants. 128 | 129 | Buffered payments average buffer size is the average size in bytes of the 130 | buffers that were transmitted between participants. 131 | 132 | For more information on buffering payments, see this discussion: 133 | https://github.com/stellar/starlight/discussions/330#discussioncomment-1345257 134 | 135 | #### Participants A (US) and B (US) 136 | 137 | Latency between participants A and B was observed to be approximately 20-30ms. 138 | 139 | ##### Test AB1 140 | 141 | Test AB1 ran 10 million payments of $0.001 or less, with payments being buffered 142 | into buffers of max size 95,000. The application blocked and waited for the 143 | response of the previous buffer before sending the next buffer. 12 buffers per 144 | second and 1.1 million payments per second were witnessed. Each message was a 145 | bit more than 309KB in size. Snapshots were enabled and as such the state of the 146 | channel was being written to disk. 147 | 148 | ``` 149 | >>> payx 0.001 10000000 95000 150 | sending 0.001 payment 10000000 times 151 | time spent: 8.402299672s 152 | agreements sent: 107 153 | agreements received: 0 154 | agreements tps: 12.735 155 | buffered payments sent: 10000000 156 | buffered payments received: 0 157 | buffered payments tps: 1190150.362 158 | buffered payments max buffer size: 490698 159 | buffered payments min buffer size: 773 160 | buffered payments avg buffer size: 309420 161 | ``` 162 | 163 | ##### Test AB3 164 | 165 | Test AB3 ran 10 million payments of $0.001 or less, with payments being buffered 166 | into buffers of max size 95,000. The application blocked and waited for the 167 | response of the previous buffer before sending the next buffer. 16 buffers per 168 | second and 1.5 million payments per second were witnessed. Each message was a 169 | bit more than 490KB in size. Snapshots were not enabled and as such the state of 170 | the channel was not being written to disk. 171 | 172 | ``` 173 | >>> payx 0.001 10000000 95000 174 | sending 0.001 payment 10000000 times 175 | time spent: 6.429692082s 176 | agreements sent: 107 177 | agreements received: 0 178 | agreements tps: 16.642 179 | buffered payments sent: 10000000 180 | buffered payments received: 0 181 | buffered payments tps: 1555284.432 182 | buffered payments max buffer size: 490652 183 | buffered payments min buffer size: 544 184 | buffered payments avg buffer size: 309526 185 | ``` 186 | 187 | ##### Test AB3 188 | 189 | Test AB3 ran 10 million payments of $1000 or less, with payments being buffered 190 | into buffers of max size 95,000. The application blocked and waited for the 191 | response of the previous buffer before sending the next buffer. 16 buffers per 192 | second and 1.5 million payments per second were witnessed. Each message was a 193 | bit more than 485KB in size. Snapshots were not enabled and as such the state of 194 | the channel was not being written to disk. 195 | 196 | ``` 197 | >>> payx 1000 10000000 95000 198 | sending 1000 payment 10000000 times 199 | time spent: 6.444609094s 200 | agreements sent: 107 201 | agreements received: 0 202 | agreements tps: 16.603 203 | buffered payments sent: 10000000 204 | buffered payments received: 0 205 | buffered payments tps: 1551684.494 206 | buffered payments max buffer size: 485915 207 | buffered payments min buffer size: 768 208 | buffered payments avg buffer size: 306113 209 | ``` 210 | 211 | ##### Test AB4 212 | 213 | Test AB4 ran 1000 payments of $1 or less, with each payment being sent serially 214 | over the network. The application blocked and waited for the response before 215 | sending the next payment. 38 payments per second were witnessed. Each message 216 | was a bit more than 190 bytes in size. Snapshots were not enabled and as such 217 | the state of the channel was not being written to disk. 218 | 219 | ``` 220 | >>> payx 1 1000 1 221 | sending 1 payment 1000 times 222 | time spent: 26.247297693s 223 | agreements sent: 1000 224 | agreements received: 0 225 | agreements tps: 38.099 226 | buffered payments sent: 1000 227 | buffered payments received: 0 228 | buffered payments tps: 38.099 229 | buffered payments max buffer size: 190 230 | buffered payments min buffer size: 182 231 | buffered payments avg buffer size: 188 232 | ``` 233 | -------------------------------------------------------------------------------- /examples/console/README.md: -------------------------------------------------------------------------------- 1 | # Console Example 2 | 3 | This module contains an example using the SDK in this repository that supports a 4 | single payment channel controlled by two people through a command line interface 5 | using simple TCP network connection and JSON messages. 6 | 7 | ## State 8 | 9 | This example is a rough example and is brittle and is still a work in progress. 10 | 11 | ## Usage 12 | 13 | Run the example using the below commands. 14 | 15 | ``` 16 | git clone https://github.com/stellar/starlight 17 | cd examples/console 18 | go run . 19 | ``` 20 | 21 | Type `help` once in to discover commands to use. 22 | 23 | Foreground events and commands are written to stdout. Background events are 24 | written to stderr. To view them side-by-side pipe them to a file or alternative 25 | tty device. 26 | 27 | ## Example 28 | 29 | Run these commands on the first terminal: 30 | ``` 31 | $ go run . 32 | > listen :6000 33 | > open 34 | > deposit 100 35 | > pay 4 36 | > pay 46 37 | ``` 38 | 39 | Run these commands in the second terminal: 40 | ``` 41 | $ go run . 42 | > connect :6000 43 | > deposit 80 44 | > pay 2 45 | > declareclose 46 | > close 47 | ``` 48 | -------------------------------------------------------------------------------- /examples/console/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stellar/starlight/examples/console 2 | 3 | go 1.18 4 | 5 | replace github.com/stellar/starlight/sdk => ../../sdk 6 | 7 | require ( 8 | github.com/abiosoft/ishell v2.0.0+incompatible 9 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00 10 | github.com/stellar/go v0.0.0-20220406183204-45b6f52202f3 11 | github.com/stellar/starlight/sdk v0.0.0-00010101000000-000000000000 12 | ) 13 | 14 | require ( 15 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect 16 | github.com/chzyer/logex v1.1.10 // indirect 17 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/fatih/color v1.13.0 // indirect 20 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect 21 | github.com/go-chi/chi v4.0.3+incompatible // indirect 22 | github.com/go-errors/errors v0.0.0-20150906023321-a41850380601 // indirect 23 | github.com/google/uuid v1.2.0 // indirect 24 | github.com/gorilla/schema v1.1.0 // indirect 25 | github.com/klauspost/compress v0.0.0-20161106143436-e3b7981a12dd // indirect 26 | github.com/klauspost/cpuid v0.0.0-20160302075316-09cded8978dc // indirect 27 | github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6 // indirect 28 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect 29 | github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect 30 | github.com/mattn/go-colorable v0.1.9 // indirect 31 | github.com/mattn/go-isatty v0.0.14 // indirect 32 | github.com/pkg/errors v0.9.1 // indirect 33 | github.com/pmezard/go-difflib v1.0.0 // indirect 34 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521 // indirect 35 | github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 // indirect 36 | github.com/sirupsen/logrus v1.4.1 // indirect 37 | github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee // indirect 38 | github.com/stretchr/objx v0.3.0 // indirect 39 | github.com/stretchr/testify v1.7.0 // indirect 40 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect 41 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 42 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect 43 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /examples/console/go.sum: -------------------------------------------------------------------------------- 1 | github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= 2 | github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= 3 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= 4 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530= 5 | github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f h1:zvClvFQwU++UpIUBGC8YmDlfhUrweEy1R1Fj1gu5iIM= 6 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 7 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 8 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 9 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 14 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 15 | github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU= 16 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI= 17 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M= 18 | github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955 h1:gmtGRvSexPU4B1T/yYo0sLOKzER1YT+b4kPxPpm0Ty4= 19 | github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY= 20 | github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 21 | github.com/go-errors/errors v0.0.0-20150906023321-a41850380601 h1:jxTbmDuqQUTI6MscgbqB39vtxGfr2fi61nYIcFQUnlE= 22 | github.com/go-errors/errors v0.0.0-20150906023321-a41850380601/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 23 | github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5 h1:oERTZ1buOUYlpmKaqlO5fYmz8cZ1rYu5DieJzF4ZVmU= 24 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 25 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= 26 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 27 | github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= 28 | github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= 29 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 30 | github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= 31 | github.com/jarcoal/httpmock v0.0.0-20161210151336-4442edb3db31 h1:Aw95BEvxJ3K6o9GGv5ppCd1P8hkeIeEJ30FO+OhOJpM= 32 | github.com/klauspost/compress v0.0.0-20161106143436-e3b7981a12dd h1:vQ0EEfHpdFUtNRj1ri25MUq5jb3Vma+kKhLyjeUTVow= 33 | github.com/klauspost/compress v0.0.0-20161106143436-e3b7981a12dd/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 34 | github.com/klauspost/cpuid v0.0.0-20160302075316-09cded8978dc h1:WW8B7p7QBnFlqRVv/k6ro/S8Z7tCnYjJHcQNScx9YVs= 35 | github.com/klauspost/cpuid v0.0.0-20160302075316-09cded8978dc/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 36 | github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6 h1:KAZ1BW2TCmT6PRihDPpocIy1QTtsAsrx6TneU/4+CMg= 37 | github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= 38 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 39 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 40 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 41 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 42 | github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE= 43 | github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM= 44 | github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= 45 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 46 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 47 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 48 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 49 | github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db h1:eZgFHVkk9uOTaOQLC6tgjkzdp7Ays8eEVecBcfHZlJQ= 50 | github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= 51 | github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= 52 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 53 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 54 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 56 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00 h1:8DPul/X0IT/1TNMIxoKLwdemEOBBHDC/K4EB16Cw5WE= 57 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 58 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521 h1:3hxavr+IHMsQBrYUPQM5v0CgENFktkkbg1sfpgM3h20= 59 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubrpZHtQLnOf6EwhN2hEMusxZOhcW9H3UQ= 60 | github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 h1:S4OC0+OBKz6mJnzuHioeEat74PuQ4Sgvbf8eus695sc= 61 | github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2/go.mod h1:8zLRYR5npGjaOXgPSKat5+oOh+UHd8OdbS18iqX9F6Y= 62 | github.com/sergi/go-diff v0.0.0-20161205080420-83532ca1c1ca h1:oR/RycYTFTVXzND5r4FdsvbnBn0HJXSVeNAnwaTXRwk= 63 | github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= 64 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 65 | github.com/stellar/go v0.0.0-20220406183204-45b6f52202f3 h1:PLVF895y1tGeDj1L2Wzocl6Vn60zYCSAKtuATczbLYc= 66 | github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee h1:fbVs0xmXpBvVS4GBeiRmAE3Le70ofAqFMch1GTiq/e8= 67 | github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps= 68 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 69 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 70 | github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= 71 | github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 72 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 73 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 74 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 75 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 76 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 77 | github.com/valyala/fasthttp v0.0.0-20170109085056-0a7f0a797cd6 h1:s0IDmR1jFyWvOK7jVIuAsmHQaGkXUuTas8NXFUOwuAI= 78 | github.com/xdrpp/goxdr v0.1.1 h1:E1B2c6E8eYhOVyd7yEpOyopzTPirUeF6mVOfXfGyJyc= 79 | github.com/xeipuuv/gojsonpointer v0.0.0-20151027082146-e0fe6f683076 h1:KM4T3G70MiR+JtqplcYkNVoNz7pDwYaBxWBXQK804So= 80 | github.com/xeipuuv/gojsonreference v0.0.0-20150808065054-e02fc20de94c h1:XZWnr3bsDQWAZg4Ne+cPoXRPILrNlPNQfxBuwLl43is= 81 | github.com/xeipuuv/gojsonschema v0.0.0-20161231055540-f06f290571ce h1:cVSRGH8cOveJNwFEEZLXtB+XMnRqKLjUP6V/ZFYQCXI= 82 | github.com/yalp/jsonpath v0.0.0-20150812003900-31a79c7593bb h1:06WAhQa+mYv7BiOk13B/ywyTlkoE/S7uu6TBKU6FHnE= 83 | github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d h1:yJIizrfO599ot2kQ6Af1enICnwBD3XoxgX3MrMwot2M= 84 | github.com/yudai/golcs v0.0.0-20150405163532-d1c525dea8ce h1:888GrqRxabUce7lj4OaoShPxodm3kXOMpSa85wdYzfY= 85 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= 86 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 87 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 88 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 89 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 92 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 93 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 94 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 95 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 96 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 97 | gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0 h1:r5ptJ1tBxVAeqw4CrYWhXIMr0SybY3CDHuIbCg5CFVw= 98 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 99 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 100 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 101 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 102 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 103 | -------------------------------------------------------------------------------- /examples/console/snapshot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "time" 7 | 8 | "github.com/stellar/go/keypair" 9 | agentpkg "github.com/stellar/starlight/sdk/agent" 10 | ) 11 | 12 | type File struct { 13 | ObservationPeriodTime time.Duration 14 | ObservationPeriodLedgerGap uint32 15 | MaxOpenExpiry time.Duration 16 | ChannelAccountKey *keypair.FromAddress 17 | Snapshot agentpkg.Snapshot 18 | } 19 | 20 | type JSONFileSnapshotter struct { 21 | Filename string 22 | 23 | ObservationPeriodTime time.Duration 24 | ObservationPeriodLedgerGap uint32 25 | MaxOpenExpiry time.Duration 26 | ChannelAccountKey *keypair.FromAddress 27 | } 28 | 29 | func (j JSONFileSnapshotter) Snapshot(a *agentpkg.Agent, s agentpkg.Snapshot) { 30 | f := File{ 31 | ObservationPeriodTime: j.ObservationPeriodTime, 32 | ObservationPeriodLedgerGap: j.ObservationPeriodLedgerGap, 33 | MaxOpenExpiry: j.MaxOpenExpiry, 34 | ChannelAccountKey: j.ChannelAccountKey, 35 | Snapshot: s, 36 | } 37 | b, err := json.MarshalIndent(f, "", " ") 38 | if err != nil { 39 | panic(err) 40 | } 41 | err = ioutil.WriteFile(j.Filename, b, 0644) 42 | if err != nil { 43 | panic(err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/console/stats.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type stats struct { 13 | mu sync.RWMutex 14 | timeStart time.Time 15 | timeFinish time.Time 16 | agreementsSent int64 17 | agreementsReceived int64 18 | bufferedPaymentsSent int64 19 | bufferedPaymentsReceived int64 20 | maxBufferByteSize int 21 | minBufferByteSize int 22 | avgBufferByteSize int 23 | } 24 | 25 | func (s *stats) Reset() { 26 | s.mu.Lock() 27 | defer s.mu.Unlock() 28 | s.timeStart = time.Time{} 29 | s.timeFinish = time.Time{} 30 | s.agreementsSent = 0 31 | s.agreementsReceived = 0 32 | s.bufferedPaymentsSent = 0 33 | s.bufferedPaymentsReceived = 0 34 | s.maxBufferByteSize = 0 35 | s.minBufferByteSize = 0 36 | s.avgBufferByteSize = 0 37 | } 38 | 39 | func (s *stats) Clone() *stats { 40 | s.mu.RLock() 41 | defer s.mu.RUnlock() 42 | return &stats{ 43 | agreementsSent: s.agreementsSent, 44 | agreementsReceived: s.agreementsReceived, 45 | bufferedPaymentsSent: s.bufferedPaymentsSent, 46 | bufferedPaymentsReceived: s.bufferedPaymentsReceived, 47 | maxBufferByteSize: s.maxBufferByteSize, 48 | minBufferByteSize: s.minBufferByteSize, 49 | } 50 | } 51 | 52 | func (s *stats) MarkStart() { 53 | s.mu.Lock() 54 | defer s.mu.Unlock() 55 | if !s.timeStart.IsZero() { 56 | panic("marking start of stats when already marked") 57 | } 58 | s.timeStart = time.Now() 59 | } 60 | 61 | func (s *stats) MarkFinish() { 62 | s.mu.Lock() 63 | defer s.mu.Unlock() 64 | if !s.timeFinish.IsZero() { 65 | panic("marking finish of stats when already marked") 66 | } 67 | s.timeFinish = time.Now() 68 | } 69 | 70 | func (s *stats) AddAgreementsSent(delta int) { 71 | s.mu.Lock() 72 | defer s.mu.Unlock() 73 | s.agreementsSent += int64(delta) 74 | } 75 | 76 | func (s *stats) AddAgreementsReceived(delta int) { 77 | s.mu.Lock() 78 | defer s.mu.Unlock() 79 | s.agreementsReceived += int64(delta) 80 | } 81 | 82 | func (s *stats) AddBufferedPaymentsSent(delta int) { 83 | s.mu.Lock() 84 | defer s.mu.Unlock() 85 | s.bufferedPaymentsSent += int64(delta) 86 | } 87 | 88 | func (s *stats) AddBufferedPaymentsReceived(delta int) { 89 | s.mu.Lock() 90 | defer s.mu.Unlock() 91 | s.bufferedPaymentsReceived += int64(delta) 92 | } 93 | 94 | func (s *stats) AddBufferByteSize(size int) { 95 | s.mu.Lock() 96 | defer s.mu.Unlock() 97 | if size > s.maxBufferByteSize { 98 | s.maxBufferByteSize = size 99 | } 100 | if s.minBufferByteSize == 0 || size < s.minBufferByteSize { 101 | s.minBufferByteSize = size 102 | } 103 | s.avgBufferByteSize = (s.avgBufferByteSize + size) / 2 104 | } 105 | 106 | func (s *stats) AgreementsPerSecond() float64 { 107 | timeFinish := s.timeFinish 108 | if timeFinish.IsZero() { 109 | timeFinish = time.Now() 110 | } 111 | duration := s.timeFinish.Sub(s.timeStart) 112 | rate := float64(s.agreementsSent+s.agreementsReceived) / duration.Seconds() 113 | if math.IsNaN(rate) || math.IsInf(rate, 0) { 114 | rate = 0 115 | } 116 | return rate 117 | } 118 | 119 | func (s *stats) BufferedPaymentsPerSecond() float64 { 120 | timeFinish := s.timeFinish 121 | if timeFinish.IsZero() { 122 | timeFinish = time.Now() 123 | } 124 | duration := timeFinish.Sub(s.timeStart) 125 | rate := float64(s.bufferedPaymentsSent+s.bufferedPaymentsReceived) / duration.Seconds() 126 | if math.IsNaN(rate) || math.IsInf(rate, 0) { 127 | rate = 0 128 | } 129 | return rate 130 | } 131 | 132 | func (s *stats) Summary() string { 133 | s.mu.RLock() 134 | defer s.mu.RUnlock() 135 | sb := strings.Builder{} 136 | timeFinish := s.timeFinish 137 | if timeFinish.IsZero() { 138 | timeFinish = time.Now() 139 | } 140 | duration := timeFinish.Sub(s.timeStart) 141 | fmt.Fprintf(&sb, "time spent: %v\n", duration) 142 | fmt.Fprintf(&sb, "agreements sent: %d\n", s.agreementsSent) 143 | fmt.Fprintf(&sb, "agreements received: %d\n", s.agreementsReceived) 144 | fmt.Fprintf(&sb, "agreements tps: %.3f\n", s.AgreementsPerSecond()) 145 | fmt.Fprintf(&sb, "buffered payments sent: %d\n", s.bufferedPaymentsSent) 146 | fmt.Fprintf(&sb, "buffered payments received: %d\n", s.bufferedPaymentsReceived) 147 | fmt.Fprintf(&sb, "buffered payments tps: %.3f\n", s.BufferedPaymentsPerSecond()) 148 | fmt.Fprintf(&sb, "buffered payments max buffer size: %d\n", s.maxBufferByteSize) 149 | fmt.Fprintf(&sb, "buffered payments min buffer size: %d\n", s.minBufferByteSize) 150 | fmt.Fprintf(&sb, "buffered payments avg buffer size: %d\n", s.avgBufferByteSize) 151 | return sb.String() 152 | } 153 | 154 | func (s *stats) MarshalJSON() ([]byte, error) { 155 | s.mu.RLock() 156 | defer s.mu.RUnlock() 157 | v := struct { 158 | AgreementsSent int64 159 | AgreementsReceived int64 160 | AgreementsPerSecond int64 161 | BufferedPaymentsSent int64 162 | BufferedPaymentsReceived int64 163 | BufferedPaymentsPerSecond int64 164 | MaxBufferByteSize int 165 | MinBufferByteSize int 166 | AvgBufferByteSize int 167 | }{ 168 | AgreementsSent: s.agreementsSent, 169 | AgreementsReceived: s.agreementsReceived, 170 | AgreementsPerSecond: int64(s.AgreementsPerSecond()), 171 | BufferedPaymentsSent: s.bufferedPaymentsSent, 172 | BufferedPaymentsReceived: s.bufferedPaymentsReceived, 173 | BufferedPaymentsPerSecond: int64(s.BufferedPaymentsPerSecond()), 174 | MaxBufferByteSize: s.maxBufferByteSize, 175 | MinBufferByteSize: s.minBufferByteSize, 176 | AvgBufferByteSize: s.avgBufferByteSize, 177 | } 178 | return json.MarshalIndent(v, "", " ") 179 | } 180 | -------------------------------------------------------------------------------- /examples/console/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/stellar/go/clients/horizonclient" 7 | "github.com/stellar/go/keypair" 8 | "github.com/stellar/go/txnbuild" 9 | ) 10 | 11 | func createAccountWithRoot(client horizonclient.ClientInterface, networkPassphrase string, accountKey *keypair.FromAddress) error { 12 | rootKey := keypair.Root(networkPassphrase) 13 | root, err := client.AccountDetail(horizonclient.AccountRequest{AccountID: rootKey.Address()}) 14 | if err != nil { 15 | return err 16 | } 17 | tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ 18 | SourceAccount: &root, 19 | IncrementSequenceNum: true, 20 | BaseFee: txnbuild.MinBaseFee, 21 | Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(300)}, 22 | Operations: []txnbuild.Operation{ 23 | &txnbuild.CreateAccount{ 24 | Destination: accountKey.Address(), 25 | Amount: "10000000", 26 | }, 27 | }, 28 | }) 29 | if err != nil { 30 | return err 31 | } 32 | tx, err = tx.Sign(networkPassphrase, rootKey) 33 | if err != nil { 34 | return err 35 | } 36 | _, err = client.SubmitTransaction(tx) 37 | if err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | func retry(maxAttempts int, f func() error) (err error) { 44 | for i := 0; i < maxAttempts; i++ { 45 | err = f() 46 | if err == nil { 47 | return 48 | } 49 | time.Sleep(100 * time.Millisecond) 50 | } 51 | return err 52 | } 53 | -------------------------------------------------------------------------------- /examples/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 43 | 44 |
45 |
46 |
47 | Account Signers 48 |
49 |
50 |
51 |
52 |
53 |
54 | Balances 55 |
56 |
57 |
58 |
59 |
60 |
61 | Payments 62 |
63 |
Buffered Payments per Second
64 |
65 |
66 |
67 | 68 | 103 | 104 | 369 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.18 2 | 3 | use ( 4 | examples/console 5 | sdk 6 | ) 7 | -------------------------------------------------------------------------------- /sdk/.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | # Enable these linters in addition to the default linters that golangci-lint starts with. 3 | enable: 4 | - gofmt 5 | - gofumpt 6 | 7 | linters-settings: 8 | govet: 9 | # TODO: Enable shadow checks, because stellar/go has them enabled, but they're a bit painful for us to enable now. 10 | # check-shadowing: true 11 | gofmt: 12 | simplify: true 13 | -------------------------------------------------------------------------------- /sdk/agent/agenthttp/agenthttp.go: -------------------------------------------------------------------------------- 1 | // Package agenthttp contains a simple HTTP handler that, when requested, will 2 | // return a snapshot of an agent's snapshot at that moment. 3 | package agenthttp 4 | 5 | import ( 6 | "encoding/json" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/rs/cors" 11 | "github.com/stellar/go/keypair" 12 | "github.com/stellar/starlight/sdk/agent" 13 | ) 14 | 15 | // New creates a new http.Handler that returns snapshots for the given agent at 16 | // the root path. The snapshot generated is also accompanied by the agents 17 | // config that was used to create the agent at the time it was created. Secrets 18 | // keys in the config are transformed into public keys. 19 | func New(a *agent.Agent) http.Handler { 20 | m := http.NewServeMux() 21 | m.HandleFunc("/", handleSnapshot(a)) 22 | return cors.Default().Handler(m) 23 | } 24 | 25 | func handleSnapshot(a *agent.Agent) func(w http.ResponseWriter, r *http.Request) { 26 | return func(w http.ResponseWriter, r *http.Request) { 27 | enc := json.NewEncoder(w) 28 | enc.SetIndent("", " ") 29 | type agentConfig struct { 30 | ObservationPeriodTime time.Duration 31 | ObservationPeriodLedgerGap uint32 32 | MaxOpenExpiry time.Duration 33 | NetworkPassphrase string 34 | ChannelAccountKey *keypair.FromAddress 35 | ChannelAccountSigner *keypair.FromAddress 36 | } 37 | c := a.Config() 38 | v := struct { 39 | Config agentConfig 40 | Snapshot agent.Snapshot 41 | }{ 42 | Config: agentConfig{ 43 | ObservationPeriodTime: c.ObservationPeriodTime, 44 | ObservationPeriodLedgerGap: c.ObservationPeriodLedgerGap, 45 | MaxOpenExpiry: c.MaxOpenExpiry, 46 | NetworkPassphrase: c.NetworkPassphrase, 47 | ChannelAccountKey: c.ChannelAccountKey, 48 | ChannelAccountSigner: c.ChannelAccountSigner.FromAddress(), 49 | }, 50 | Snapshot: a.Snapshot(), 51 | } 52 | err := enc.Encode(v) 53 | if err != nil { 54 | panic(err) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /sdk/agent/bufferedagent/agent.go: -------------------------------------------------------------------------------- 1 | // Package bufferedagent contains a rudimentary and experimental implementation 2 | // of an agent that coordinates a TCP network connection, initial handshake, and 3 | // channel opens, payments, and closes, and buffers outgoing payments, 4 | // collapsing them down to a single payment. 5 | // 6 | // The agent is intended for use in examples only at this point and is not 7 | // intended to be stable or reliable. 8 | package bufferedagent 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | "io" 14 | "math" 15 | "sync" 16 | 17 | "github.com/google/uuid" 18 | "github.com/stellar/starlight/sdk/agent" 19 | "github.com/stellar/starlight/sdk/state" 20 | ) 21 | 22 | // ErrBufferFull indicates that the payment buffer has reached it's maximum size 23 | // as configured when the buffered agent was created. 24 | var ErrBufferFull = errors.New("buffer full") 25 | 26 | // Config contains the information that can be supplied to configure the Agent 27 | // at construction. 28 | type Config struct { 29 | Agent *agent.Agent 30 | AgentEvents <-chan interface{} 31 | 32 | MaxBufferSize int 33 | 34 | LogWriter io.Writer 35 | 36 | Events chan<- interface{} 37 | } 38 | 39 | // NewAgent constructs a new buffered agent with the given config. 40 | func NewAgent(c Config) *Agent { 41 | agent := &Agent{ 42 | agent: c.Agent, 43 | agentEvents: c.AgentEvents, 44 | 45 | maxbufferSize: c.MaxBufferSize, 46 | 47 | logWriter: c.LogWriter, 48 | 49 | bufferReady: make(chan struct{}, 1), 50 | sendingReady: make(chan struct{}, 1), 51 | idle: make(chan struct{}), 52 | 53 | events: c.Events, 54 | } 55 | bufferReadyCloseOnce := sync.Once{} 56 | agent.bufferReadyClose = func() { 57 | bufferReadyCloseOnce.Do(func() { 58 | close(agent.bufferReady) 59 | }) 60 | } 61 | agent.resetbuffer() 62 | agent.sendingReady <- struct{}{} 63 | go agent.flushLoop() 64 | return agent 65 | } 66 | 67 | // Agent coordinates a payment channel over a TCP connection, and 68 | // buffers payments by collapsing them down into single payments while it waits 69 | // for a chance to make the next payment. 70 | // 71 | // All functions of the Agent are safe to call from multiple goroutines as they 72 | // use an internal mutex. 73 | type Agent struct { 74 | maxbufferSize int 75 | 76 | logWriter io.Writer 77 | 78 | agentEvents <-chan interface{} 79 | events chan<- interface{} 80 | 81 | // mu is a lock for the mutable fields of this type. It should be locked 82 | // when reading or writing any of the mutable fields. The mutable fields are 83 | // listed below. If pushing to a chan, such as Events, it is unnecessary to 84 | // lock. 85 | mu sync.Mutex 86 | 87 | agent *agent.Agent 88 | 89 | bufferID string 90 | buffer []BufferedPayment 91 | bufferTotalAmount int64 92 | bufferReady chan struct{} 93 | bufferReadyClose func() 94 | sendingReady chan struct{} 95 | idle chan struct{} 96 | } 97 | 98 | // MaxBufferSize returns the maximum buffer size that was configured at 99 | // construction or changed with SetMaxBufferSize. The maximum buffer size is the 100 | // maximum number of payments that can be buffered while waiting for the 101 | // opportunity to include the buffered payments in an agreement. 102 | func (a *Agent) MaxBufferSize() int { 103 | a.mu.Lock() 104 | defer a.mu.Unlock() 105 | return a.maxbufferSize 106 | } 107 | 108 | // SetMaxBufferSize sets and changes the maximum buffer size. 109 | func (a *Agent) SetMaxBufferSize(maxbufferSize int) { 110 | a.mu.Lock() 111 | defer a.mu.Unlock() 112 | a.maxbufferSize = maxbufferSize 113 | } 114 | 115 | // Open opens the channel for the given asset. The open is coordinated with the 116 | // other participant. An immediate error may be indicated if the attempt to open 117 | // was immediately unsuccessful. However, more likely any error will be returned 118 | // on the events channel as the process involves the other participant. 119 | func (a *Agent) Open(asset state.Asset) error { 120 | a.mu.Lock() 121 | defer a.mu.Unlock() 122 | return a.agent.Open(asset) 123 | } 124 | 125 | // PaymentWithMemo buffers a payment which will be paid in the next agreement. 126 | // The identifier for the buffer is returned. An error may be returned 127 | // immediately if the buffer is full. Any errors relating to the payment, and 128 | // confirmation of the payment, will be returned asynchronously on the events 129 | // channel. 130 | func (a *Agent) PaymentWithMemo(paymentAmount int64, memo string) (bufferID string, err error) { 131 | a.mu.Lock() 132 | defer a.mu.Unlock() 133 | if a.maxbufferSize != 0 && len(a.buffer) == a.maxbufferSize { 134 | return "", ErrBufferFull 135 | } 136 | if paymentAmount > math.MaxInt64-a.bufferTotalAmount { 137 | return "", ErrBufferFull 138 | } 139 | a.buffer = append(a.buffer, BufferedPayment{Amount: paymentAmount, Memo: memo}) 140 | a.bufferTotalAmount += paymentAmount 141 | bufferID = a.bufferID 142 | select { 143 | case a.bufferReady <- struct{}{}: 144 | default: 145 | } 146 | return 147 | } 148 | 149 | // Payment is equivalent to calling PaymentWithMemo with an empty memo. 150 | func (a *Agent) Payment(paymentAmount int64) (bufferID string, err error) { 151 | return a.PaymentWithMemo(paymentAmount, "") 152 | } 153 | 154 | // Wait waits for sending of all buffered payments to complete and the buffer to 155 | // be empty. It can be called multiple times, and it can be called in between 156 | // sends of new payments. 157 | func (a *Agent) Wait() { 158 | <-a.idle 159 | } 160 | 161 | // DeclareClose starts the close process of the channel by submitting the latest 162 | // declaration to the network, then coordinating an immediate close with the 163 | // other participant. If an immediate close can be coordinated it will 164 | // automatically occur, otherwise a participant must call Close after the 165 | // observation period has passed to close the channel. 166 | // 167 | // It is not possible to make new payments once called. 168 | func (a *Agent) DeclareClose() error { 169 | a.mu.Lock() 170 | defer a.mu.Unlock() 171 | a.bufferReadyClose() 172 | return a.agent.DeclareClose() 173 | } 174 | 175 | // Close submits the close transaction to the network. DeclareClose must have 176 | // been called by one of the participants before hand. 177 | func (a *Agent) Close() error { 178 | a.mu.Lock() 179 | defer a.mu.Unlock() 180 | a.bufferReadyClose() 181 | return a.agent.Close() 182 | } 183 | 184 | func (a *Agent) eventLoop() { 185 | defer close(a.events) 186 | defer close(a.sendingReady) 187 | defer fmt.Fprintf(a.logWriter, "event loop stopped\n") 188 | fmt.Fprintf(a.logWriter, "event loop started\n") 189 | for { 190 | ae, open := <-a.agentEvents 191 | if !open { 192 | break 193 | } 194 | 195 | // Pass all agent events up to the agent. 196 | a.events <- ae 197 | 198 | // Unpack payment received and sent events and create events for each 199 | // sub-payment that was bufferd within them. 200 | switch e := ae.(type) { 201 | case agent.PaymentReceivedEvent: 202 | memo := bufferedPaymentsMemo{} 203 | err := memo.UnmarshalBinary(e.CloseAgreement.Envelope.Details.Memo) 204 | if err != nil { 205 | a.events <- agent.ErrorEvent{Err: err} 206 | continue 207 | } 208 | a.events <- BufferedPaymentsReceivedEvent{ 209 | BufferID: memo.ID, 210 | BufferByteSize: len(e.CloseAgreement.Envelope.Details.Memo), 211 | Payments: memo.Payments, 212 | } 213 | case agent.PaymentSentEvent: 214 | a.sendingReady <- struct{}{} 215 | memo := bufferedPaymentsMemo{} 216 | err := memo.UnmarshalBinary(e.CloseAgreement.Envelope.Details.Memo) 217 | if err != nil { 218 | a.events <- agent.ErrorEvent{Err: err} 219 | continue 220 | } 221 | a.events <- BufferedPaymentsSentEvent{ 222 | BufferID: memo.ID, 223 | BufferByteSize: len(e.CloseAgreement.Envelope.Details.Memo), 224 | Payments: memo.Payments, 225 | } 226 | } 227 | } 228 | } 229 | 230 | func (a *Agent) flushLoop() { 231 | defer fmt.Fprintf(a.logWriter, "flush loop stopped\n") 232 | fmt.Fprintf(a.logWriter, "flush loop started\n") 233 | for { 234 | _, open := <-a.sendingReady 235 | if !open { 236 | return 237 | } 238 | select { 239 | case _, open = <-a.bufferReady: 240 | if !open { 241 | return 242 | } 243 | a.flush() 244 | default: 245 | select { 246 | case _, open = <-a.bufferReady: 247 | if !open { 248 | return 249 | } 250 | a.flush() 251 | case a.idle <- struct{}{}: 252 | a.sendingReady <- struct{}{} 253 | } 254 | } 255 | } 256 | } 257 | 258 | func (a *Agent) flush() { 259 | var bufferID string 260 | var buffer []BufferedPayment 261 | var bufferTotalAmount int64 262 | 263 | func() { 264 | a.mu.Lock() 265 | defer a.mu.Unlock() 266 | 267 | bufferID = a.bufferID 268 | buffer = a.buffer 269 | bufferTotalAmount = a.bufferTotalAmount 270 | a.resetbuffer() 271 | }() 272 | 273 | if len(buffer) == 0 { 274 | a.sendingReady <- struct{}{} 275 | return 276 | } 277 | 278 | memo := bufferedPaymentsMemo{ 279 | ID: bufferID, 280 | Payments: buffer, 281 | } 282 | memoBytes, err := memo.MarshalBinary() 283 | if err != nil { 284 | a.events <- agent.ErrorEvent{Err: err} 285 | a.sendingReady <- struct{}{} 286 | return 287 | } 288 | 289 | err = a.agent.PaymentWithMemo(bufferTotalAmount, memoBytes) 290 | if err != nil { 291 | a.events <- agent.ErrorEvent{Err: err} 292 | a.sendingReady <- struct{}{} 293 | return 294 | } 295 | } 296 | 297 | func (a *Agent) resetbuffer() { 298 | a.bufferID = uuid.NewString() 299 | a.buffer = nil 300 | a.bufferTotalAmount = 0 301 | } 302 | -------------------------------------------------------------------------------- /sdk/agent/bufferedagent/buffered_memo.go: -------------------------------------------------------------------------------- 1 | package bufferedagent 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | 8 | "github.com/klauspost/compress/gzip" 9 | ) 10 | 11 | type bufferedPaymentsMemo struct { 12 | ID string 13 | Payments []BufferedPayment 14 | } 15 | 16 | func (m *bufferedPaymentsMemo) MarshalBinary() ([]byte, error) { 17 | b := bytes.Buffer{} 18 | z, err := gzip.NewWriterLevel(&b, gzip.BestSpeed) 19 | if err != nil { 20 | panic(fmt.Errorf("creating gzip writer: %w", err)) 21 | } 22 | enc := gob.NewEncoder(z) 23 | type bpm bufferedPaymentsMemo 24 | err = enc.Encode((*bpm)(m)) 25 | if err != nil { 26 | return nil, fmt.Errorf("encoding buffered payments memo: %w", err) 27 | } 28 | z.Close() 29 | return b.Bytes(), nil 30 | } 31 | 32 | func (m *bufferedPaymentsMemo) UnmarshalBinary(b []byte) error { 33 | r := bytes.NewReader(b) 34 | z, err := gzip.NewReader(r) 35 | if err != nil { 36 | return fmt.Errorf("creating gzip reader: %w", err) 37 | } 38 | dec := gob.NewDecoder(z) 39 | type bpm bufferedPaymentsMemo 40 | err = dec.Decode((*bpm)(m)) 41 | if err != nil { 42 | return fmt.Errorf("decoding buffered payments memo: %w", err) 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /sdk/agent/bufferedagent/buffered_payment.go: -------------------------------------------------------------------------------- 1 | package bufferedagent 2 | 3 | // BufferedPayment contains the details of a payment that is buffered and 4 | // transmitted in the memo of an agreement on the payment channel. 5 | type BufferedPayment struct { 6 | Amount int64 7 | Memo string 8 | } 9 | -------------------------------------------------------------------------------- /sdk/agent/bufferedagent/events.go: -------------------------------------------------------------------------------- 1 | package bufferedagent 2 | 3 | import "github.com/stellar/starlight/sdk/agent" 4 | 5 | // BufferedPaymentReceivedEvent occurs when a payment is received that was 6 | // buffered. 7 | type BufferedPaymentsReceivedEvent struct { 8 | agent.PaymentReceivedEvent 9 | BufferID string 10 | BufferByteSize int 11 | Payments []BufferedPayment 12 | } 13 | 14 | // BufferedPaymentSentEvent occurs when a payment is sent that was buffered. 15 | type BufferedPaymentsSentEvent struct { 16 | agent.PaymentSentEvent 17 | BufferID string 18 | BufferByteSize int 19 | Payments []BufferedPayment 20 | } 21 | -------------------------------------------------------------------------------- /sdk/agent/bufferedagent/tcp.go: -------------------------------------------------------------------------------- 1 | package bufferedagent 2 | 3 | // ServeTCP listens on the given address for a single incoming connection to 4 | // start a payment channel. 5 | func (a *Agent) ServeTCP(addr string) error { 6 | go a.eventLoop() 7 | return a.agent.ServeTCP(addr) 8 | } 9 | 10 | // ConnectTCP connects to the given address for establishing a single payment 11 | // channel. 12 | func (a *Agent) ConnectTCP(addr string) error { 13 | go a.eventLoop() 14 | return a.agent.ConnectTCP(addr) 15 | } 16 | -------------------------------------------------------------------------------- /sdk/agent/events.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "github.com/stellar/go/keypair" 5 | "github.com/stellar/starlight/sdk/state" 6 | ) 7 | 8 | // ErrorEvent occurs when an error has occurred, and contains the error 9 | // occurred. 10 | type ErrorEvent struct { 11 | Err error 12 | } 13 | 14 | // ConnectedEvent occurs when the agent is connected to another participant. 15 | type ConnectedEvent struct { 16 | ChannelAccount *keypair.FromAddress 17 | Signer *keypair.FromAddress 18 | } 19 | 20 | // OpenedEvent occurs when the channel has been opened. 21 | type OpenedEvent struct { 22 | OpenAgreement state.OpenAgreement 23 | } 24 | 25 | // PaymentReceivedEvent occurs when a payment is received and the balance it 26 | // agrees to would be the resulting disbursements from the channel if closed. 27 | type PaymentReceivedEvent struct { 28 | CloseAgreement state.CloseAgreement 29 | } 30 | 31 | // PaymentSentEvent occurs when a payment is sent and the other participant has 32 | // confirmed it such that the balance the agreement agrees to would be the 33 | // resulting disbursements from the channel if closed. 34 | type PaymentSentEvent struct { 35 | CloseAgreement state.CloseAgreement 36 | } 37 | 38 | // ClosingEvent occurs when the channel is closing and no new payments should be 39 | // proposed or confirmed. 40 | type ClosingEvent struct{} 41 | 42 | // ClosingWithOutdatedStateEvent occurs when the channel is closing and no new payments should be 43 | // proposed or confirmed, and the state it is closing in is not the latest known state. 44 | type ClosingWithOutdatedStateEvent struct{} 45 | 46 | // ClosedEvent occurs when the channel is successfully closed. 47 | type ClosedEvent struct{} 48 | -------------------------------------------------------------------------------- /sdk/agent/hash.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/stellar/go/txnbuild" 7 | ) 8 | 9 | func hashTx(txXDR, networkPassphrase string) (string, error) { 10 | tx, err := txnbuild.TransactionFromXDR(txXDR) 11 | if err != nil { 12 | return "", fmt.Errorf("parsing transaction xdr: %w", err) 13 | } 14 | if feeBump, ok := tx.FeeBump(); ok { 15 | hash, err := feeBump.HashHex(networkPassphrase) 16 | if err != nil { 17 | return "", fmt.Errorf("hashing fee bump tx: %w", err) 18 | } 19 | return hash, nil 20 | } 21 | if transaction, ok := tx.Transaction(); ok { 22 | hash, err := transaction.HashHex(networkPassphrase) 23 | if err != nil { 24 | return "", fmt.Errorf("hashing tx: %w", err) 25 | } 26 | return hash, nil 27 | } 28 | return "", fmt.Errorf("transaction unrecognized") 29 | } 30 | -------------------------------------------------------------------------------- /sdk/agent/horizon/balance_collector.go: -------------------------------------------------------------------------------- 1 | package horizon 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/stellar/go/amount" 7 | "github.com/stellar/go/clients/horizonclient" 8 | "github.com/stellar/go/keypair" 9 | "github.com/stellar/go/protocols/horizon" 10 | "github.com/stellar/starlight/sdk/agent" 11 | "github.com/stellar/starlight/sdk/state" 12 | ) 13 | 14 | var _ agent.BalanceCollector = &BalanceCollector{} 15 | 16 | // BalanceCollector implements an agent's interface for collecting balances by 17 | // querying Horizon's accounts endpoint for the balance. 18 | type BalanceCollector struct { 19 | HorizonClient horizonclient.ClientInterface 20 | } 21 | 22 | // GetBalance queries Horizon for the balance of the given asset on the given 23 | // account. 24 | func (h *BalanceCollector) GetBalance(accountID *keypair.FromAddress, asset state.Asset) (int64, error) { 25 | var account horizon.Account 26 | account, err := h.HorizonClient.AccountDetail(horizonclient.AccountRequest{AccountID: accountID.Address()}) 27 | if err != nil { 28 | return 0, fmt.Errorf("getting account details of %s: %w", accountID, err) 29 | } 30 | for _, b := range account.Balances { 31 | if b.Asset.Code == asset.Code() || b.Asset.Issuer == asset.Issuer() { 32 | balance, err := amount.ParseInt64(account.Balances[0].Balance) 33 | if err != nil { 34 | return 0, fmt.Errorf("parsing %s balance of %s: %w", asset, accountID, err) 35 | } 36 | return balance, nil 37 | } 38 | } 39 | return 0, nil 40 | } 41 | -------------------------------------------------------------------------------- /sdk/agent/horizon/doc.go: -------------------------------------------------------------------------------- 1 | // Package horizon contains types that implement a variety of interfaces defined 2 | // by the sdk/agent and its sub-packages, providing the functionality of those 3 | // interfaces by querying Horizon. 4 | package horizon 5 | -------------------------------------------------------------------------------- /sdk/agent/horizon/sequence_number_collector.go: -------------------------------------------------------------------------------- 1 | package horizon 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/stellar/go/clients/horizonclient" 7 | "github.com/stellar/go/keypair" 8 | "github.com/stellar/starlight/sdk/agent" 9 | ) 10 | 11 | var _ agent.SequenceNumberCollector = &SequenceNumberCollector{} 12 | 13 | // SequenceNumberCollector implements an agent's interface for collecting the 14 | // current sequence number by querying Horizon's accounts endpoint. 15 | type SequenceNumberCollector struct { 16 | HorizonClient horizonclient.ClientInterface 17 | } 18 | 19 | // GetSequenceNumber queries Horizon for the balance of the given account. 20 | func (h *SequenceNumberCollector) GetSequenceNumber(accountID *keypair.FromAddress) (int64, error) { 21 | account, err := h.HorizonClient.AccountDetail(horizonclient.AccountRequest{AccountID: accountID.Address()}) 22 | if err != nil { 23 | return 0, fmt.Errorf("getting account details of %s: %w", accountID, err) 24 | } 25 | seqNum, err := account.GetSequenceNumber() 26 | if err != nil { 27 | return 0, fmt.Errorf("getting sequence number of account %s: %w", accountID, err) 28 | } 29 | return seqNum, nil 30 | } 31 | -------------------------------------------------------------------------------- /sdk/agent/horizon/streamer.go: -------------------------------------------------------------------------------- 1 | package horizon 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "sync" 7 | 8 | "github.com/stellar/go/clients/horizonclient" 9 | "github.com/stellar/go/keypair" 10 | "github.com/stellar/go/protocols/horizon" 11 | "github.com/stellar/starlight/sdk/agent" 12 | ) 13 | 14 | var _ agent.Streamer = &Streamer{} 15 | 16 | // Streamer implements the agent's interface for streaming transactions that 17 | // affect a set of accounts, by using the streaming endpoints of Horizon's API 18 | // to collect new transactions as they occur. 19 | type Streamer struct { 20 | HorizonClient horizonclient.ClientInterface 21 | ErrorHandler func(error) 22 | } 23 | 24 | // StreamTx streams transactions that affect the given accounts, sending each 25 | // transaction to the txs channel returned. StreamTx can be stopped by calling 26 | // the cancel function returned. If multiple accounts are given the same 27 | // transaction it may be broadcasted in duplicate if the transaction affects more 28 | // than one account being monitored. The given cursor suppors resuming a 29 | // previous stream. 30 | // 31 | // TODO: Improve StreamTx so that it only streams transactions that affect the 32 | // given accounts. At the moment, to reduce complexity and due to limitations in 33 | // Horizon, it streams all network transactions. See 34 | // https://github.com/stellar/go/issues/3874. 35 | func (h *Streamer) StreamTx(cursor string, accounts ...*keypair.FromAddress) (txs <-chan agent.StreamedTransaction, cancel func()) { 36 | // txsCh is the channel that streamed transactions will be written to. 37 | txsCh := make(chan agent.StreamedTransaction) 38 | 39 | // cancelCh will be used to signal the streamer to stop. 40 | cancelCh := make(chan struct{}) 41 | 42 | // Start a streamer that will write txs and stop when 43 | // signaled to cancel. 44 | go func() { 45 | defer close(txsCh) 46 | h.streamTx(cursor, txsCh, cancelCh) 47 | }() 48 | 49 | cancelOnce := sync.Once{} 50 | cancel = func() { 51 | cancelOnce.Do(func() { 52 | close(cancelCh) 53 | }) 54 | } 55 | return txsCh, cancel 56 | } 57 | 58 | func (h *Streamer) streamTx(cursor string, txs chan<- agent.StreamedTransaction, cancel <-chan struct{}) { 59 | ctx, ctxCancel := context.WithCancel(context.Background()) 60 | go func() { 61 | <-cancel 62 | ctxCancel() 63 | }() 64 | for { 65 | req := horizonclient.TransactionRequest{ 66 | Cursor: cursor, 67 | } 68 | err := h.HorizonClient.StreamTransactions(ctx, req, func(tx horizon.Transaction) { 69 | pagingToken := tx.PagingToken() 70 | txOrderID, err := strconv.ParseInt(pagingToken, 10, 64) 71 | if err != nil { 72 | ctxCancel() 73 | if h.ErrorHandler != nil { 74 | h.ErrorHandler(err) 75 | } 76 | return 77 | } 78 | cursor = pagingToken 79 | streamedTx := agent.StreamedTransaction{ 80 | Cursor: cursor, 81 | TransactionOrderID: txOrderID, 82 | TransactionXDR: tx.EnvelopeXdr, 83 | ResultXDR: tx.ResultXdr, 84 | ResultMetaXDR: tx.ResultMetaXdr, 85 | } 86 | select { 87 | case <-cancel: 88 | ctxCancel() 89 | case txs <- streamedTx: 90 | } 91 | }) 92 | if err == nil { 93 | break 94 | } 95 | if h.ErrorHandler != nil { 96 | h.ErrorHandler(err) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /sdk/agent/horizon/streamer_test.go: -------------------------------------------------------------------------------- 1 | package horizon 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stellar/go/clients/horizonclient" 9 | "github.com/stellar/go/keypair" 10 | "github.com/stellar/go/protocols/horizon" 11 | "github.com/stellar/starlight/sdk/agent" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/mock" 14 | ) 15 | 16 | func TestStreamer_StreamTx_longBlock(t *testing.T) { 17 | client := &horizonclient.MockClient{} 18 | h := Streamer{HorizonClient: client} 19 | 20 | accountA := keypair.MustRandom() 21 | client.On( 22 | "StreamTransactions", 23 | mock.Anything, 24 | horizonclient.TransactionRequest{}, 25 | mock.Anything, 26 | ).Return(nil).Run(func(args mock.Arguments) { 27 | ctx := args[0].(context.Context) 28 | handler := args[2].(horizonclient.TransactionHandler) 29 | handler(horizon.Transaction{ 30 | PT: "1", 31 | EnvelopeXdr: "a-txxdr", 32 | ResultXdr: "a-resultxdr", 33 | ResultMetaXdr: "a-resultmetaxdr", 34 | }) 35 | // Simulate long block on new data from Horizon. 36 | <-ctx.Done() 37 | }) 38 | 39 | t.Log("Streaming...") 40 | txsCh, cancel := h.StreamTx("", accountA.FromAddress()) 41 | 42 | // Pull streamed transactions into slice. 43 | t.Log("Pulling some transactions from stream...") 44 | txs := []agent.StreamedTransaction{} 45 | txs = append(txs, <-txsCh) 46 | 47 | // Check that the streamed transactions has transactions from A and B. 48 | assert.ElementsMatch( 49 | t, 50 | []agent.StreamedTransaction{ 51 | { 52 | Cursor: "1", 53 | TransactionOrderID: 1, 54 | TransactionXDR: "a-txxdr", 55 | ResultXDR: "a-resultxdr", 56 | ResultMetaXDR: "a-resultmetaxdr", 57 | }, 58 | }, 59 | txs, 60 | ) 61 | 62 | // Cancel streaming, and check that multiple cancels are okay. 63 | t.Log("Canceling...") 64 | cancel() 65 | cancel() 66 | 67 | // Check that the transaction stream channel is closed. It may still be 68 | // producing transactions for a short period of time. 69 | open := true 70 | for open { 71 | _, open = <-txsCh 72 | t.Log("Still open, waiting for cancel...") 73 | } 74 | assert.False(t, open, "txs channel not closed but should be after cancel called") 75 | } 76 | 77 | func TestStreamer_StreamTx_manyTxs(t *testing.T) { 78 | client := &horizonclient.MockClient{} 79 | h := Streamer{HorizonClient: client} 80 | 81 | accountB := keypair.MustRandom() 82 | client.On( 83 | "StreamTransactions", 84 | mock.Anything, 85 | horizonclient.TransactionRequest{}, 86 | mock.Anything, 87 | ).Return(nil).Run(func(args mock.Arguments) { 88 | ctx := args[0].(context.Context) 89 | handler := args[2].(horizonclient.TransactionHandler) 90 | // Simulate many transactions coming from Horizon. 91 | for { 92 | select { 93 | case <-ctx.Done(): 94 | return 95 | default: 96 | handler(horizon.Transaction{ 97 | PT: "1", 98 | EnvelopeXdr: "b-txxdr", 99 | ResultXdr: "b-resultxdr", 100 | ResultMetaXdr: "b-resultmetaxdr", 101 | }) 102 | } 103 | } 104 | }) 105 | 106 | t.Log("Streaming...") 107 | txsCh, cancel := h.StreamTx("", accountB.FromAddress()) 108 | 109 | // Pull streamed transactions into slice. 110 | t.Log("Pulling some transactions from stream...") 111 | txs := []agent.StreamedTransaction{} 112 | txs = append(txs, <-txsCh, <-txsCh) 113 | 114 | // Check that the streamed transactions has transactions from A and B. 115 | assert.ElementsMatch( 116 | t, 117 | []agent.StreamedTransaction{ 118 | { 119 | Cursor: "1", 120 | TransactionOrderID: 1, 121 | TransactionXDR: "b-txxdr", 122 | ResultXDR: "b-resultxdr", 123 | ResultMetaXDR: "b-resultmetaxdr", 124 | }, 125 | { 126 | Cursor: "1", 127 | TransactionOrderID: 1, 128 | TransactionXDR: "b-txxdr", 129 | ResultXDR: "b-resultxdr", 130 | ResultMetaXDR: "b-resultmetaxdr", 131 | }, 132 | }, 133 | txs, 134 | ) 135 | 136 | // Cancel streaming, and check that multiple cancels are okay. 137 | t.Log("Canceling...") 138 | cancel() 139 | cancel() 140 | 141 | // Check that the transaction stream channel is closed. It may still be 142 | // producing transactions for a short period of time. 143 | open := true 144 | for open { 145 | _, open = <-txsCh 146 | t.Log("Still open, waiting for cancel...") 147 | } 148 | assert.False(t, open, "txs channel not closed but should be after cancel called") 149 | } 150 | 151 | func TestHorizonStreamer_StreamTx_error(t *testing.T) { 152 | client := &horizonclient.MockClient{} 153 | 154 | errorsSeen := make(chan error, 1) 155 | h := Streamer{ 156 | HorizonClient: client, 157 | ErrorHandler: func(err error) { 158 | errorsSeen <- err 159 | }, 160 | } 161 | 162 | accountB := keypair.MustRandom() 163 | 164 | // Simulate an error occuring while streaming. 165 | client.On( 166 | "StreamTransactions", 167 | mock.Anything, 168 | horizonclient.TransactionRequest{}, 169 | mock.Anything, 170 | ).Return(errors.New("an error")).Run(func(args mock.Arguments) { 171 | handler := args[2].(horizonclient.TransactionHandler) 172 | handler(horizon.Transaction{ 173 | PT: "1", 174 | EnvelopeXdr: "a-txxdr", 175 | ResultXdr: "a-resultxdr", 176 | ResultMetaXdr: "a-resultmetaxdr", 177 | }) 178 | }).Once() 179 | 180 | // Simulator no error after retrying. 181 | client.On( 182 | "StreamTransactions", 183 | mock.Anything, 184 | horizonclient.TransactionRequest{Cursor: "1"}, 185 | mock.Anything, 186 | ).Return(nil).Run(func(args mock.Arguments) { 187 | ctx := args[0].(context.Context) 188 | handler := args[2].(horizonclient.TransactionHandler) 189 | handler(horizon.Transaction{ 190 | PT: "2", 191 | EnvelopeXdr: "b-txxdr", 192 | ResultXdr: "b-resultxdr", 193 | ResultMetaXdr: "b-resultmetaxdr", 194 | }) 195 | <-ctx.Done() 196 | }).Once() 197 | 198 | t.Log("Streaming...") 199 | txsCh, cancel := h.StreamTx("", accountB.FromAddress()) 200 | 201 | // Pull streamed transactions into slice. 202 | t.Log("Pulling some transactions from stream...") 203 | txs := []agent.StreamedTransaction{} 204 | txs = append(txs, <-txsCh) 205 | assert.EqualError(t, <-errorsSeen, "an error") 206 | txs = append(txs, <-txsCh) 207 | 208 | // Check that the streamed transactions has transactions from A and B. 209 | assert.ElementsMatch( 210 | t, 211 | []agent.StreamedTransaction{ 212 | { 213 | Cursor: "1", 214 | TransactionOrderID: 1, 215 | TransactionXDR: "a-txxdr", 216 | ResultXDR: "a-resultxdr", 217 | ResultMetaXDR: "a-resultmetaxdr", 218 | }, 219 | { 220 | Cursor: "2", 221 | TransactionOrderID: 2, 222 | TransactionXDR: "b-txxdr", 223 | ResultXDR: "b-resultxdr", 224 | ResultMetaXDR: "b-resultmetaxdr", 225 | }, 226 | }, 227 | txs, 228 | ) 229 | 230 | // Cancel streaming, and check that multiple cancels are okay. 231 | t.Log("Canceling...") 232 | cancel() 233 | cancel() 234 | 235 | // Check that the transaction stream channel is closed. 236 | _, open := <-txsCh 237 | assert.False(t, open, "txs channel not closed but should be after cancel called") 238 | } 239 | -------------------------------------------------------------------------------- /sdk/agent/horizon/submitter.go: -------------------------------------------------------------------------------- 1 | package horizon 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/stellar/go/clients/horizonclient" 7 | "github.com/stellar/starlight/sdk/agent/submit" 8 | ) 9 | 10 | var _ submit.SubmitTxer = &Submitter{} 11 | 12 | // Submitter implements an submit's interface for submitting transaction XDRs to 13 | // the network, via Horizon's API. 14 | type Submitter struct { 15 | HorizonClient horizonclient.ClientInterface 16 | } 17 | 18 | // SubmitTx submits the given xdr as a transaction to Horizon. 19 | func (h *Submitter) SubmitTx(xdr string) error { 20 | _, err := h.HorizonClient.SubmitTransactionXDR(xdr) 21 | if err != nil { 22 | return fmt.Errorf("submitting tx %s: %w", xdr, buildErr(err)) 23 | } 24 | return nil 25 | } 26 | 27 | func buildErr(err error) error { 28 | if hErr := horizonclient.GetError(err); hErr != nil { 29 | resultString, rErr := hErr.ResultString() 30 | if rErr != nil { 31 | resultString = "" 32 | } 33 | return fmt.Errorf("%w (%v)", err, resultString) 34 | } 35 | return err 36 | } 37 | -------------------------------------------------------------------------------- /sdk/agent/ingest.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/stellar/starlight/sdk/state" 8 | ) 9 | 10 | var ingestingFinished = errors.New("ingesting finished") 11 | 12 | func (a *Agent) ingest() error { 13 | tx, ok := <-a.streamerTransactions 14 | if !ok { 15 | return ingestingFinished 16 | } 17 | 18 | a.mu.Lock() 19 | defer a.mu.Unlock() 20 | 21 | txHash, err := hashTx(tx.TransactionXDR, a.networkPassphrase) 22 | if err != nil { 23 | err = fmt.Errorf("ingesting tx (cursor=%s): hashing tx: %w", tx.Cursor, err) 24 | a.events <- ErrorEvent{Err: err} 25 | return err 26 | } 27 | fmt.Fprintf(a.logWriter, "ingesting cursor: %s tx: %s\n", tx.Cursor, txHash) 28 | 29 | stateBefore, err := a.channel.State() 30 | if err != nil { 31 | err = fmt.Errorf("ingesting tx (cursor=%s hash=%s): getting channel state before: %w", tx.Cursor, txHash, err) 32 | a.events <- ErrorEvent{Err: err} 33 | return err 34 | } 35 | fmt.Fprintf(a.logWriter, "state before: %v\n", stateBefore) 36 | 37 | defer a.takeSnapshot() 38 | 39 | err = a.channel.IngestTx(tx.TransactionOrderID, tx.TransactionXDR, tx.ResultXDR, tx.ResultMetaXDR) 40 | if err != nil { 41 | err = fmt.Errorf("ingesting tx (cursor=%s hash=%s): ingesting xdr: %w", tx.Cursor, txHash, err) 42 | a.events <- ErrorEvent{Err: err} 43 | return err 44 | } 45 | 46 | stateAfter, err := a.channel.State() 47 | if err != nil { 48 | err = fmt.Errorf("ingesting tx (cursor=%s hash=%s): getting channel state after: %w", tx.Cursor, txHash, err) 49 | a.events <- ErrorEvent{Err: err} 50 | return err 51 | } 52 | fmt.Fprintf(a.logWriter, "state after: %v\n", stateAfter) 53 | 54 | if a.events != nil { 55 | if stateAfter != stateBefore { 56 | fmt.Fprintf(a.logWriter, "writing event: %v\n", stateAfter) 57 | switch stateAfter { 58 | case state.StateOpen: 59 | a.events <- OpenedEvent{a.channel.OpenAgreement()} 60 | case state.StateClosing: 61 | a.events <- ClosingEvent{} 62 | case state.StateClosingWithOutdatedState: 63 | a.events <- ClosingWithOutdatedStateEvent{} 64 | case state.StateClosed: 65 | a.streamerCancel() 66 | a.events <- ClosedEvent{} 67 | } 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func (a *Agent) ingestLoop() { 75 | for { 76 | err := a.ingest() 77 | if err != nil { 78 | fmt.Fprintf(a.logWriter, "error ingesting: %v\n", err) 79 | } 80 | if errors.Is(err, ingestingFinished) { 81 | break 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /sdk/agent/msg/msg.go: -------------------------------------------------------------------------------- 1 | // Package msg contains simple types to assist with transmitting and 2 | // communicating across a network about a payment channel between two 3 | // participants. It is rather rudimentary and intended for use in examples. 4 | // There are no stability or compatibility guarantees for the messages or 5 | // encoding format used. 6 | package msg 7 | 8 | import ( 9 | "encoding/gob" 10 | "io" 11 | 12 | "github.com/stellar/go/keypair" 13 | "github.com/stellar/starlight/sdk/state" 14 | ) 15 | 16 | // Type is the message type, used to indicate which message is contained inside 17 | // a Message. 18 | type Type int 19 | 20 | const ( 21 | TypeHello Type = 10 22 | TypeOpenRequest Type = 20 23 | TypeOpenResponse Type = 21 24 | TypePaymentRequest Type = 30 25 | TypePaymentResponse Type = 31 26 | TypeCloseRequest Type = 40 27 | TypeCloseResponse Type = 41 28 | ) 29 | 30 | // Message is a message that can be transmitted to support two participants in a 31 | // payment channel communicating by signaling who they are with a hello, opening 32 | // the channel, making payments, and closing the channel. 33 | type Message struct { 34 | Type Type 35 | 36 | Hello *Hello 37 | 38 | OpenRequest *state.OpenEnvelope 39 | OpenResponse *state.OpenSignatures 40 | 41 | PaymentRequest *state.CloseEnvelope 42 | PaymentResponse *state.CloseSignatures 43 | 44 | CloseRequest *state.CloseEnvelope 45 | CloseResponse *state.CloseSignatures 46 | } 47 | 48 | // Hello can be used to signal to another participant a minimal amount of 49 | // information the other participant needs to know about them. 50 | type Hello struct { 51 | ChannelAccount keypair.FromAddress 52 | Signer keypair.FromAddress 53 | } 54 | 55 | // Encoder is an encoder that can be used to encode messages. 56 | // It is currently set as the encoding/gob.Encoder, but may be changed to 57 | // another type at anytime to facilitate testing or to improve performance. 58 | type Encoder = gob.Encoder 59 | 60 | func NewEncoder(w io.Writer) *Encoder { 61 | return gob.NewEncoder(w) 62 | } 63 | 64 | type Decoder = gob.Decoder 65 | 66 | func NewDecoder(r io.Reader) *Decoder { 67 | return gob.NewDecoder(r) 68 | } 69 | -------------------------------------------------------------------------------- /sdk/agent/submit/submitter.go: -------------------------------------------------------------------------------- 1 | // Package submit contains a type and logic for submitting transactions that need to be fee bumped. 2 | // 3 | // The Submitter type can be used to wrap any submission logic, and it will 4 | // check if the transactions fee is below a threshold that indicates it needs to 5 | // be fee bumped, and if so, it will wrap it in a fee bump transaction before 6 | // submission. 7 | // 8 | // This package is intended for use in example payment channel implementations. 9 | package submit 10 | 11 | import ( 12 | "fmt" 13 | 14 | "github.com/stellar/go/clients/horizonclient" 15 | "github.com/stellar/go/keypair" 16 | "github.com/stellar/go/txnbuild" 17 | ) 18 | 19 | // SubmitTxer is an implementation of submitting transaction XDR to the network. 20 | type SubmitTxer interface { 21 | SubmitTx(xdr string) error 22 | } 23 | 24 | // Submitter submits transactions to the network via Horizon. If a transaction 25 | // has a base fee below the submitters base fee, the transaction is wrapped in a 26 | // fee bump transaction. This means fee-less transactions are wrapped in fee 27 | // bump transaction. 28 | // 29 | // The BaseFee is the base fee that will be used for any submission where the 30 | // transaction has a lower base fee. 31 | type Submitter struct { 32 | SubmitTxer SubmitTxer 33 | NetworkPassphrase string 34 | BaseFee int64 35 | FeeAccount *keypair.FromAddress 36 | FeeAccountSigners []*keypair.Full 37 | } 38 | 39 | // SubmitTx submits the transaction. If the transaction has a base fee that is 40 | // lower than the submitters base fee it is wrapped in a fee bump transaction 41 | // with the Submitter's FeeAccount as the fee account. 42 | func (s *Submitter) SubmitTx(tx *txnbuild.Transaction) error { 43 | if tx.BaseFee() < s.BaseFee { 44 | return s.submitTxWithFeeBump(tx) 45 | } 46 | return s.submitTx(tx) 47 | } 48 | 49 | func (s *Submitter) submitTx(tx *txnbuild.Transaction) error { 50 | txeBase64, err := tx.Base64() 51 | if err != nil { 52 | return fmt.Errorf("encoding tx as base64: %w", err) 53 | } 54 | err = s.SubmitTxer.SubmitTx(txeBase64) 55 | if err != nil { 56 | return fmt.Errorf("submitting tx: %w", buildErr(err)) 57 | } 58 | return nil 59 | } 60 | 61 | func (s *Submitter) submitTxWithFeeBump(tx *txnbuild.Transaction) error { 62 | feeBumpTx, err := txnbuild.NewFeeBumpTransaction(txnbuild.FeeBumpTransactionParams{ 63 | Inner: tx, 64 | BaseFee: s.BaseFee, 65 | FeeAccount: s.FeeAccount.Address(), 66 | }) 67 | if err != nil { 68 | return fmt.Errorf("building fee bump tx: %w", err) 69 | } 70 | feeBumpTx, err = feeBumpTx.Sign(s.NetworkPassphrase, s.FeeAccountSigners...) 71 | if err != nil { 72 | return fmt.Errorf("signing fee bump tx: %w", err) 73 | } 74 | txeBase64, err := feeBumpTx.Base64() 75 | if err != nil { 76 | return fmt.Errorf("encoding fee bump tx as base64: %w", err) 77 | } 78 | err = s.SubmitTxer.SubmitTx(txeBase64) 79 | if err != nil { 80 | return fmt.Errorf("submitting fee bump tx: %w", buildErr(err)) 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func buildErr(err error) error { 87 | if hErr := horizonclient.GetError(err); hErr != nil { 88 | resultString, rErr := hErr.ResultString() 89 | if rErr != nil { 90 | resultString = "" 91 | } 92 | return fmt.Errorf("%w (%v)", err, resultString) 93 | } 94 | return err 95 | } 96 | -------------------------------------------------------------------------------- /sdk/agent/tcp.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | // ServeTCP listens on the given address for a single incoming connection to 9 | // start a payment channel. 10 | func (a *Agent) ServeTCP(addr string) error { 11 | if a.conn != nil { 12 | return fmt.Errorf("already connected") 13 | } 14 | ln, err := net.Listen("tcp", addr) 15 | if err != nil { 16 | return fmt.Errorf("listening on %s: %w", addr, err) 17 | } 18 | conn, err := ln.Accept() 19 | if err != nil { 20 | return fmt.Errorf("accepting incoming connection: %w", err) 21 | } 22 | fmt.Fprintf(a.logWriter, "accepted connection from %v\n", conn.RemoteAddr()) 23 | a.conn = conn 24 | err = a.hello() 25 | if err != nil { 26 | return fmt.Errorf("sending hello: %w", err) 27 | } 28 | go a.receiveLoop() 29 | return nil 30 | } 31 | 32 | // ConnectTCP connects to the given address for establishing a single payment 33 | // channel. 34 | func (a *Agent) ConnectTCP(addr string) error { 35 | if a.conn != nil { 36 | return fmt.Errorf("already connected") 37 | } 38 | var err error 39 | conn, err := net.Dial("tcp", addr) 40 | if err != nil { 41 | return fmt.Errorf("connecting to %s: %w", addr, err) 42 | } 43 | fmt.Fprintf(a.logWriter, "connected to %v\n", conn.RemoteAddr()) 44 | a.conn = conn 45 | err = a.hello() 46 | if err != nil { 47 | return fmt.Errorf("sending hello: %w", err) 48 | } 49 | go a.receiveLoop() 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /sdk/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stellar/starlight/sdk 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/google/gofuzz v1.2.0 7 | github.com/google/uuid v1.2.0 8 | github.com/klauspost/compress v0.0.0-20161106143436-e3b7981a12dd 9 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00 10 | github.com/stellar/go v0.0.0-20220419042134-9f968df09eda 11 | github.com/stretchr/objx v0.3.0 // indirect 12 | github.com/stretchr/testify v1.7.0 13 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 14 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/go-chi/chi v4.0.3+incompatible // indirect 20 | github.com/go-errors/errors v0.0.0-20150906023321-a41850380601 // indirect 21 | github.com/gorilla/schema v1.1.0 // indirect 22 | github.com/klauspost/cpuid v0.0.0-20160302075316-09cded8978dc // indirect 23 | github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6 // indirect 24 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect 25 | github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect 26 | github.com/pkg/errors v0.9.1 // indirect 27 | github.com/pmezard/go-difflib v1.0.0 // indirect 28 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521 // indirect 29 | github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 // indirect 30 | github.com/sirupsen/logrus v1.4.1 // indirect 31 | github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee // indirect 32 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect 33 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect 34 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 35 | gopkg.in/yaml.v2 v2.3.0 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /sdk/go.sum: -------------------------------------------------------------------------------- 1 | github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f h1:zvClvFQwU++UpIUBGC8YmDlfhUrweEy1R1Fj1gu5iIM= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU= 6 | github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955 h1:gmtGRvSexPU4B1T/yYo0sLOKzER1YT+b4kPxPpm0Ty4= 7 | github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY= 8 | github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 9 | github.com/go-errors/errors v0.0.0-20150906023321-a41850380601 h1:jxTbmDuqQUTI6MscgbqB39vtxGfr2fi61nYIcFQUnlE= 10 | github.com/go-errors/errors v0.0.0-20150906023321-a41850380601/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 11 | github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5 h1:oERTZ1buOUYlpmKaqlO5fYmz8cZ1rYu5DieJzF4ZVmU= 12 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 13 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 14 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= 15 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 16 | github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= 17 | github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= 18 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 19 | github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= 20 | github.com/jarcoal/httpmock v0.0.0-20161210151336-4442edb3db31 h1:Aw95BEvxJ3K6o9GGv5ppCd1P8hkeIeEJ30FO+OhOJpM= 21 | github.com/klauspost/compress v0.0.0-20161106143436-e3b7981a12dd h1:vQ0EEfHpdFUtNRj1ri25MUq5jb3Vma+kKhLyjeUTVow= 22 | github.com/klauspost/compress v0.0.0-20161106143436-e3b7981a12dd/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 23 | github.com/klauspost/cpuid v0.0.0-20160302075316-09cded8978dc h1:WW8B7p7QBnFlqRVv/k6ro/S8Z7tCnYjJHcQNScx9YVs= 24 | github.com/klauspost/cpuid v0.0.0-20160302075316-09cded8978dc/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 25 | github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6 h1:KAZ1BW2TCmT6PRihDPpocIy1QTtsAsrx6TneU/4+CMg= 26 | github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= 27 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 28 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 29 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 30 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 31 | github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE= 32 | github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM= 33 | github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db h1:eZgFHVkk9uOTaOQLC6tgjkzdp7Ays8eEVecBcfHZlJQ= 34 | github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= 35 | github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= 36 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 37 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 38 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 39 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 40 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00 h1:8DPul/X0IT/1TNMIxoKLwdemEOBBHDC/K4EB16Cw5WE= 41 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 42 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521 h1:3hxavr+IHMsQBrYUPQM5v0CgENFktkkbg1sfpgM3h20= 43 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubrpZHtQLnOf6EwhN2hEMusxZOhcW9H3UQ= 44 | github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 h1:S4OC0+OBKz6mJnzuHioeEat74PuQ4Sgvbf8eus695sc= 45 | github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2/go.mod h1:8zLRYR5npGjaOXgPSKat5+oOh+UHd8OdbS18iqX9F6Y= 46 | github.com/sergi/go-diff v0.0.0-20161205080420-83532ca1c1ca h1:oR/RycYTFTVXzND5r4FdsvbnBn0HJXSVeNAnwaTXRwk= 47 | github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= 48 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 49 | github.com/stellar/go v0.0.0-20220406183204-45b6f52202f3 h1:PLVF895y1tGeDj1L2Wzocl6Vn60zYCSAKtuATczbLYc= 50 | github.com/stellar/go v0.0.0-20220419042134-9f968df09eda h1:wIBsNGn+W8ZjFFY5ScqU2p1IDsCOSp9eUtC0+FYqikE= 51 | github.com/stellar/go v0.0.0-20220419042134-9f968df09eda/go.mod h1:XDw7zGAJCmEuZVmcr7PvLioASsqQRN80dL+OkOxV50A= 52 | github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee h1:fbVs0xmXpBvVS4GBeiRmAE3Le70ofAqFMch1GTiq/e8= 53 | github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps= 54 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 55 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 56 | github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= 57 | github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 58 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 59 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 60 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 61 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 62 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 63 | github.com/valyala/fasthttp v0.0.0-20170109085056-0a7f0a797cd6 h1:s0IDmR1jFyWvOK7jVIuAsmHQaGkXUuTas8NXFUOwuAI= 64 | github.com/xdrpp/goxdr v0.1.1 h1:E1B2c6E8eYhOVyd7yEpOyopzTPirUeF6mVOfXfGyJyc= 65 | github.com/xeipuuv/gojsonpointer v0.0.0-20151027082146-e0fe6f683076 h1:KM4T3G70MiR+JtqplcYkNVoNz7pDwYaBxWBXQK804So= 66 | github.com/xeipuuv/gojsonreference v0.0.0-20150808065054-e02fc20de94c h1:XZWnr3bsDQWAZg4Ne+cPoXRPILrNlPNQfxBuwLl43is= 67 | github.com/xeipuuv/gojsonschema v0.0.0-20161231055540-f06f290571ce h1:cVSRGH8cOveJNwFEEZLXtB+XMnRqKLjUP6V/ZFYQCXI= 68 | github.com/yalp/jsonpath v0.0.0-20150812003900-31a79c7593bb h1:06WAhQa+mYv7BiOk13B/ywyTlkoE/S7uu6TBKU6FHnE= 69 | github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d h1:yJIizrfO599ot2kQ6Af1enICnwBD3XoxgX3MrMwot2M= 70 | github.com/yudai/golcs v0.0.0-20150405163532-d1c525dea8ce h1:888GrqRxabUce7lj4OaoShPxodm3kXOMpSa85wdYzfY= 71 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= 72 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 73 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 74 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 75 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 76 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 77 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 79 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 80 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 81 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 83 | gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0 h1:r5ptJ1tBxVAeqw4CrYWhXIMr0SybY3CDHuIbCg5CFVw= 84 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 85 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 86 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 87 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 89 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 90 | -------------------------------------------------------------------------------- /sdk/state/asset.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/stellar/go/txnbuild" 8 | "github.com/stellar/go/xdr" 9 | ) 10 | 11 | // Asset is a Stellar asset. 12 | type Asset string 13 | 14 | const NativeAsset = Asset("native") 15 | 16 | // IsNative returns true if the asset is the native asset of the stellar 17 | // network. 18 | func (a Asset) IsNative() bool { 19 | return a.Asset().IsNative() 20 | } 21 | 22 | // Code returns the asset code of credit assets. 23 | func (a Asset) Code() string { 24 | return a.Asset().GetCode() 25 | } 26 | 27 | // Issuer returns the issuer of credit assets. 28 | func (a Asset) Issuer() string { 29 | return a.Asset().GetIssuer() 30 | } 31 | 32 | // Asset returns an asset from the stellar/go/txnbuild package with the 33 | // same asset code and issuer, or a native asset if a native asset. 34 | func (a Asset) Asset() txnbuild.Asset { 35 | parts := strings.SplitN(string(a), ":", 2) 36 | if len(parts) == 1 { 37 | return txnbuild.NativeAsset{} 38 | } 39 | return txnbuild.CreditAsset{ 40 | Code: parts[0], 41 | Issuer: parts[1], 42 | } 43 | } 44 | 45 | // StringCanonical returns a string friendly representation of the asset in 46 | // canonical form. 47 | func (a Asset) StringCanonical() string { 48 | if a.IsNative() { 49 | return xdr.AssetTypeToString[xdr.AssetTypeAssetTypeNative] 50 | } 51 | return fmt.Sprintf("%s:%s", a.Code(), a.Issuer()) 52 | } 53 | 54 | // EqualTrustLineAsset returns true if the canonical strings of this asset and a 55 | // given trustline asset are equal, else false. 56 | func (a Asset) EqualTrustLineAsset(ta xdr.TrustLineAsset) bool { 57 | switch ta.Type { 58 | case xdr.AssetTypeAssetTypeNative, xdr.AssetTypeAssetTypeCreditAlphanum4, xdr.AssetTypeAssetTypeCreditAlphanum12: 59 | return ta.ToAsset().StringCanonical() == a.StringCanonical() 60 | } 61 | return false 62 | } 63 | -------------------------------------------------------------------------------- /sdk/state/asset_test.go: -------------------------------------------------------------------------------- 1 | package state_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stellar/go/txnbuild" 8 | "github.com/stellar/starlight/sdk/state" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestAsset(t *testing.T) { 13 | testCases := []struct { 14 | Asset state.Asset 15 | WantTxnbuildAsset txnbuild.Asset 16 | WantIsNative bool 17 | WantCode string 18 | WantIssuer string 19 | }{ 20 | {state.Asset(""), txnbuild.NativeAsset{}, true, "", ""}, 21 | {state.Asset("native"), txnbuild.NativeAsset{}, true, "", ""}, 22 | {state.NativeAsset, txnbuild.NativeAsset{}, true, "", ""}, 23 | {state.Asset(":"), txnbuild.CreditAsset{}, false, "", ""}, 24 | {state.Asset("ABCD:GABCD"), txnbuild.CreditAsset{Code: "ABCD", Issuer: "GABCD"}, false, "ABCD", "GABCD"}, 25 | {state.Asset("ABCD:GABCD:AB"), txnbuild.CreditAsset{Code: "ABCD", Issuer: "GABCD:AB"}, false, "ABCD", "GABCD:AB"}, 26 | } 27 | for _, tc := range testCases { 28 | t.Run(fmt.Sprint(tc.Asset), func(t *testing.T) { 29 | assert.Equal(t, tc.WantTxnbuildAsset, tc.Asset.Asset()) 30 | assert.Equal(t, tc.WantIsNative, tc.Asset.IsNative()) 31 | assert.Equal(t, tc.WantCode, tc.Asset.Code()) 32 | assert.Equal(t, tc.WantIssuer, tc.Asset.Issuer()) 33 | }) 34 | } 35 | } 36 | 37 | func TestStringCanonical(t *testing.T) { 38 | testCases := []struct { 39 | Asset state.Asset 40 | WantStringCanonical string 41 | }{ 42 | {state.Asset(""), "native"}, 43 | {state.Asset("native"), "native"}, 44 | {state.NativeAsset, "native"}, 45 | {state.Asset(":"), ":"}, 46 | {state.Asset("ABCD:GABCD"), "ABCD:GABCD"}, 47 | {state.Asset("ABCD:GABCD:AB"), "ABCD:GABCD:AB"}, 48 | } 49 | for _, tc := range testCases { 50 | t.Run(fmt.Sprint(tc.Asset), func(t *testing.T) { 51 | assert.Equal(t, tc.WantStringCanonical, tc.Asset.StringCanonical()) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /sdk/state/close.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/stellar/go/txnbuild" 7 | "github.com/stellar/starlight/sdk/txbuild" 8 | ) 9 | 10 | // closeTxs builds the transactions that can be submitted to close the channel 11 | // with the state defined in the CloseAgreementDetails, and that was opened with 12 | // the given OpenAgreementDetails. If the channel has previous build these close 13 | // transactions and still has them stored internally then it will return those 14 | // previously built transactions, otherwise the transactions will be built from 15 | // scratch. 16 | func (c *Channel) closeTxs(oad OpenDetails, d CloseDetails) (txs CloseTransactions, err error) { 17 | if c.openAgreement.Envelope.Details.Equal(oad) { 18 | if c.latestAuthorizedCloseAgreement.Envelope.Details.Equal(d) { 19 | return c.latestAuthorizedCloseAgreement.Transactions, nil 20 | } 21 | if c.latestUnauthorizedCloseAgreement.Envelope.Details.Equal(d) { 22 | return c.latestUnauthorizedCloseAgreement.Transactions, nil 23 | } 24 | } 25 | txClose, err := txbuild.Close(txbuild.CloseParams{ 26 | ObservationPeriodTime: d.ObservationPeriodTime, 27 | ObservationPeriodLedgerGap: d.ObservationPeriodLedgerGap, 28 | InitiatorSigner: c.initiatorSigner(), 29 | ResponderSigner: c.responderSigner(), 30 | InitiatorChannelAccount: c.initiatorChannelAccount().Address, 31 | ResponderChannelAccount: c.responderChannelAccount().Address, 32 | StartSequence: oad.StartingSequence, 33 | IterationNumber: d.IterationNumber, 34 | AmountToInitiator: amountToInitiator(d.Balance), 35 | AmountToResponder: amountToResponder(d.Balance), 36 | Asset: oad.Asset.Asset(), 37 | }) 38 | if err != nil { 39 | return CloseTransactions{}, err 40 | } 41 | txCloseHash, err := txClose.Hash(c.networkPassphrase) 42 | if err != nil { 43 | return CloseTransactions{}, err 44 | } 45 | txDecl, err := txbuild.Declaration(txbuild.DeclarationParams{ 46 | InitiatorChannelAccount: c.initiatorChannelAccount().Address, 47 | StartSequence: oad.StartingSequence, 48 | IterationNumber: d.IterationNumber, 49 | IterationNumberExecuted: 0, 50 | ConfirmingSigner: d.ConfirmingSigner, 51 | CloseTxHash: txCloseHash, 52 | }) 53 | if err != nil { 54 | return CloseTransactions{}, err 55 | } 56 | txDeclHash, err := txDecl.Hash(c.networkPassphrase) 57 | if err != nil { 58 | return CloseTransactions{}, err 59 | } 60 | txs = CloseTransactions{ 61 | DeclarationHash: txDeclHash, 62 | Declaration: txDecl, 63 | CloseHash: txCloseHash, 64 | Close: txClose, 65 | } 66 | return txs, nil 67 | } 68 | 69 | // CloseTxs builds the declaration and close transactions used for closing the 70 | // channel using the latest close agreement. The transactions are signed and 71 | // ready to submit. 72 | func (c *Channel) CloseTxs() (declTx *txnbuild.Transaction, closeTx *txnbuild.Transaction, err error) { 73 | cae := c.latestAuthorizedCloseAgreement 74 | txs := cae.SignedTransactions() 75 | return txs.Declaration, txs.Close, nil 76 | } 77 | 78 | // ProposeClose proposes that the latest authorized close agreement be submitted 79 | // without waiting the observation period. This should be used when participants 80 | // are in agreement on the final close state, but would like to submit earlier 81 | // than the original observation time. 82 | func (c *Channel) ProposeClose() (CloseAgreement, error) { 83 | // If an unfinished unauthorized agreement exists, error. 84 | if !c.latestUnauthorizedCloseAgreement.Envelope.Empty() { 85 | return CloseAgreement{}, fmt.Errorf("cannot propose coordinated close while an unfinished payment exists") 86 | } 87 | 88 | // If the channel is not open yet, error. 89 | if c.latestAuthorizedCloseAgreement.Envelope.Empty() || !c.openExecutedAndValidated { 90 | return CloseAgreement{}, fmt.Errorf("cannot propose a coordinated close before channel is opened") 91 | } 92 | 93 | d := c.latestAuthorizedCloseAgreement.Envelope.Details 94 | d.ObservationPeriodTime = 0 95 | d.ObservationPeriodLedgerGap = 0 96 | d.ProposingSigner = c.localSigner.FromAddress() 97 | d.ConfirmingSigner = c.remoteSigner 98 | 99 | txs, err := c.closeTxs(c.openAgreement.Envelope.Details, d) 100 | if err != nil { 101 | return CloseAgreement{}, fmt.Errorf("making declaration and close transactions: %w", err) 102 | } 103 | sigs, err := signCloseAgreementTxs(txs, c.localSigner) 104 | if err != nil { 105 | return CloseAgreement{}, fmt.Errorf("signing open agreement with local: %w", err) 106 | } 107 | 108 | // Store the close agreement while participants iterate on signatures. 109 | c.latestUnauthorizedCloseAgreement = CloseAgreement{ 110 | Envelope: CloseEnvelope{ 111 | Details: d, 112 | ProposerSignatures: sigs, 113 | }, 114 | Transactions: txs, 115 | } 116 | return c.latestUnauthorizedCloseAgreement, nil 117 | } 118 | 119 | func (c *Channel) validateClose(ca CloseEnvelope) error { 120 | // If the channel is not open yet, error. 121 | if c.latestAuthorizedCloseAgreement.Envelope.Empty() || !c.openExecutedAndValidated { 122 | return fmt.Errorf("cannot confirm a coordinated close before channel is opened") 123 | } 124 | if ca.Details.IterationNumber != c.latestAuthorizedCloseAgreement.Envelope.Details.IterationNumber { 125 | return fmt.Errorf("close agreement iteration number does not match saved latest authorized close agreement") 126 | } 127 | if ca.Details.Balance != c.latestAuthorizedCloseAgreement.Envelope.Details.Balance { 128 | return fmt.Errorf("close agreement balance does not match saved latest authorized close agreement") 129 | } 130 | if ca.Details.ObservationPeriodTime != 0 { 131 | return fmt.Errorf("close agreement observation period time is not zero") 132 | } 133 | if ca.Details.ObservationPeriodLedgerGap != 0 { 134 | return fmt.Errorf("close agreement observation period ledger gap is not zero") 135 | } 136 | if !ca.Details.ConfirmingSigner.Equal(c.localSigner.FromAddress()) && !ca.Details.ConfirmingSigner.Equal(c.remoteSigner) { 137 | return fmt.Errorf("close agreement confirmer does not match a local or remote signer, got: %s", ca.Details.ConfirmingSigner.Address()) 138 | } 139 | return nil 140 | } 141 | 142 | // ConfirmClose agrees to a close agreement to be submitted without waiting the 143 | // observation period. The agreement will always be accepted if it is identical 144 | // to the latest authorized close agreement, and it is signed by the participant 145 | // proposing the close. 146 | func (c *Channel) ConfirmClose(ce CloseEnvelope) (closeAgreement CloseAgreement, err error) { 147 | err = c.validateClose(ce) 148 | if err != nil { 149 | return CloseAgreement{}, fmt.Errorf("validating close agreement: %w", err) 150 | } 151 | 152 | txs, err := c.closeTxs(c.openAgreement.Envelope.Details, ce.Details) 153 | if err != nil { 154 | return CloseAgreement{}, fmt.Errorf("making close transactions: %w", err) 155 | } 156 | 157 | remoteSigs := ce.SignaturesFor(c.remoteSigner) 158 | if remoteSigs == nil { 159 | return CloseAgreement{}, fmt.Errorf("remote is not a signer") 160 | } 161 | 162 | localSigs := ce.SignaturesFor(c.localSigner.FromAddress()) 163 | if localSigs == nil { 164 | return CloseAgreement{}, fmt.Errorf("local is not a signer") 165 | } 166 | 167 | // If remote has not signed the txs or signatures is invalid, or the local 168 | // signatures if present are invalid, error as is invalid. 169 | verifyInputs := []signatureVerificationInput{ 170 | {TransactionHash: txs.DeclarationHash, Signature: remoteSigs.Declaration, Signer: c.remoteSigner}, 171 | {TransactionHash: txs.CloseHash, Signature: remoteSigs.Close, Signer: c.remoteSigner}, 172 | } 173 | if !localSigs.Empty() { 174 | verifyInputs = append(verifyInputs, []signatureVerificationInput{ 175 | {TransactionHash: txs.DeclarationHash, Signature: localSigs.Declaration, Signer: c.localSigner.FromAddress()}, 176 | {TransactionHash: txs.CloseHash, Signature: localSigs.Close, Signer: c.localSigner.FromAddress()}, 177 | }...) 178 | } 179 | err = verifySignatures(verifyInputs) 180 | if err != nil { 181 | return CloseAgreement{}, fmt.Errorf("invalid signature: %w", err) 182 | } 183 | 184 | // If local has not signed close, check that the payment is not to the proposer, then sign. 185 | if localSigs.Empty() { 186 | // If the local is not the confirmer, do not sign, because being the 187 | // proposer they should have signed earlier. 188 | if !ce.Details.ConfirmingSigner.Equal(c.localSigner.FromAddress()) { 189 | return CloseAgreement{}, fmt.Errorf("not signed by local") 190 | } 191 | ce.ConfirmerSignatures, err = signCloseAgreementTxs(txs, c.localSigner) 192 | if err != nil { 193 | return CloseAgreement{}, fmt.Errorf("local signing: %w", err) 194 | } 195 | } 196 | 197 | // The new close agreement is valid and authorized, store and promote it. 198 | c.latestAuthorizedCloseAgreement = CloseAgreement{ 199 | Envelope: ce, 200 | Transactions: txs, 201 | } 202 | c.latestUnauthorizedCloseAgreement = CloseAgreement{} 203 | return c.latestAuthorizedCloseAgreement, nil 204 | } 205 | -------------------------------------------------------------------------------- /sdk/state/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package state contains a state machine, contained in the Channel type, for 3 | managing a Starlight payment channel. 4 | 5 | The Channel type once constructed contains functions for three categories of 6 | operations: 7 | - Open: Opening the payment channel. 8 | - Payment: Making a payment from either participant to the other participant. 9 | - Close: Coordinating an immediate close the payment channel. 10 | 11 | The Channel type also provides functions for ingesting data from the network 12 | into the state machine. This is necessary to progress the state of the channel 13 | through states that are based on network activity, such as open and close. 14 | 15 | The Open, Payment, and Close operations are broken up into three steps: 16 | - Propose: Called by the payer to create the agreement. 17 | - Confirm: Called by the payee to confirm the agreement. 18 | - Finalize*: Called by the payer to finalize the agreement with the payees 19 | signatures. 20 | 21 | +-----------+ +-----------+ 22 | | Payer | | Payee | 23 | +-----+-----+ +-----+-----+ 24 | | | 25 | Propose | 26 | +----------------->+ 27 | | Confirm 28 | +<-----------------+ 29 | Finalize* | 30 | | | 31 | 32 | * Note that the Open and Close processes do not have a Finalize operation, and the 33 | Confirm is used in its place at this time. A Finalize operation is likely to be 34 | added in the future. 35 | 36 | None of the primitives in this package are threadsafe and synchronization 37 | must be provided by the caller if the package is used in a concurrent 38 | context. 39 | */ 40 | package state 41 | -------------------------------------------------------------------------------- /sdk/state/hash.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | ) 7 | 8 | type TransactionHash [32]byte 9 | 10 | func (h TransactionHash) String() string { 11 | return hex.EncodeToString(h[:]) 12 | } 13 | 14 | func (h TransactionHash) MarshalText() ([]byte, error) { 15 | text := [len(h) * 2]byte{} 16 | n := hex.Encode(text[:], h[:]) 17 | if n != len(text) { 18 | return nil, hex.ErrLength 19 | } 20 | return text[:], nil 21 | } 22 | 23 | func (h *TransactionHash) UnmarshalText(text []byte) error { 24 | if len(text) != len(h)*2 { 25 | return fmt.Errorf("unmarshaling transaction hash: input length %d expected %d", len(text), len(h)*2) 26 | } 27 | n, err := hex.Decode(h[:], text) 28 | if err != nil { 29 | return fmt.Errorf("unmarshaling transaction hash: %w", err) 30 | } 31 | if n != len(h) { 32 | return fmt.Errorf("unmarshaling transaction hash: decoded length %d expected %d", n, len(h)) 33 | } 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /sdk/state/hash_test.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestHash_String(t *testing.T) { 11 | h := TransactionHash{} 12 | assert.Equal( 13 | t, 14 | "0000000000000000000000000000000000000000000000000000000000000000", 15 | h.String(), 16 | ) 17 | h = TransactionHash{0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01} 18 | assert.Equal( 19 | t, 20 | "0123456789012345678901234567890101234567890123456789012345678901", 21 | h.String(), 22 | ) 23 | } 24 | 25 | func TestHash_MarshalText(t *testing.T) { 26 | h := TransactionHash{} 27 | b, err := h.MarshalText() 28 | require.NoError(t, err) 29 | wantB := []byte("0000000000000000000000000000000000000000000000000000000000000000") 30 | assert.Equal(t, wantB, b) 31 | 32 | h = TransactionHash{0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01} 33 | b, err = h.MarshalText() 34 | require.NoError(t, err) 35 | wantB = []byte("0123456789012345678901234567890101234567890123456789012345678901") 36 | assert.Equal(t, wantB, b) 37 | } 38 | 39 | func TestHash_UnmarshalText(t *testing.T) { 40 | // Zero. 41 | s := "0000000000000000000000000000000000000000000000000000000000000000" 42 | h := TransactionHash{} 43 | err := h.UnmarshalText([]byte(s)) 44 | require.NoError(t, err) 45 | wantH := TransactionHash{} 46 | assert.Equal(t, wantH, h) 47 | 48 | // Valid. 49 | s = "0123456789012345678901234567890101234567890123456789012345678901" 50 | h = TransactionHash{} 51 | err = h.UnmarshalText([]byte(s)) 52 | require.NoError(t, err) 53 | wantH = TransactionHash{0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01} 54 | assert.Equal(t, wantH, h) 55 | 56 | // Invalid: too long by one character / a nibble. 57 | s = "01234567890123456789012345678901012345678901234567890123456789000" 58 | h = TransactionHash{} 59 | err = h.UnmarshalText([]byte(s)) 60 | assert.EqualError(t, err, "unmarshaling transaction hash: input length 65 expected 64") 61 | 62 | // Invalid: too long by two characters / a byte. 63 | s = "012345678901234567890123456789010123456789012345678901234567890000" 64 | h = TransactionHash{} 65 | err = h.UnmarshalText([]byte(s)) 66 | assert.EqualError(t, err, "unmarshaling transaction hash: input length 66 expected 64") 67 | 68 | // Invalid: too short by one character / a nibble. 69 | s = "012345678901234567890123456789010123456789012345678901234567890" 70 | h = TransactionHash{} 71 | err = h.UnmarshalText([]byte(s)) 72 | assert.EqualError(t, err, "unmarshaling transaction hash: input length 63 expected 64") 73 | 74 | // Invalid: too short by two characters / a byte. 75 | s = "01234567890123456789012345678901012345678901234567890123456789" 76 | h = TransactionHash{} 77 | err = h.UnmarshalText([]byte(s)) 78 | assert.EqualError(t, err, "unmarshaling transaction hash: input length 62 expected 64") 79 | } 80 | -------------------------------------------------------------------------------- /sdk/state/integrationtests/helpers_test.go: -------------------------------------------------------------------------------- 1 | package integrationtests 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/binary" 6 | "testing" 7 | "time" 8 | 9 | stellarAmount "github.com/stellar/go/amount" 10 | "github.com/stellar/go/clients/horizonclient" 11 | "github.com/stellar/go/keypair" 12 | "github.com/stellar/go/protocols/horizon" 13 | "github.com/stellar/go/txnbuild" 14 | "github.com/stellar/starlight/sdk/state" 15 | "github.com/stellar/starlight/sdk/txbuild" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | // functions to be used in the sdk/state/integrationtests integration tests 20 | 21 | type AssetParam struct { 22 | Asset state.Asset 23 | Distributor *keypair.Full 24 | } 25 | 26 | func initAccounts(t *testing.T, assetParam AssetParam) (initiator Participant, responder Participant) { 27 | initiator = Participant{ 28 | Name: "Initiator", 29 | KP: keypair.MustRandom(), 30 | ChannelAccount: keypair.MustRandom(), 31 | Contribution: 1_000_0000000, 32 | } 33 | t.Log("Initiator:", initiator.KP.Address()) 34 | t.Log("Initiator ChannelAccount:", initiator.ChannelAccount.Address()) 35 | { 36 | err := retry(t, 2, func() error { return createAccount(initiator.KP.FromAddress()) }) 37 | require.NoError(t, err) 38 | err = retry(t, 2, func() error { 39 | return fundAsset(assetParam.Asset, initiator.Contribution, initiator.KP, assetParam.Distributor) 40 | }) 41 | require.NoError(t, err) 42 | 43 | t.Log("Initiator Contribution:", initiator.Contribution, "of asset:", assetParam.Asset.Code(), "issuer: ", assetParam.Asset.Issuer()) 44 | initChannelAccount(t, &initiator, assetParam) 45 | } 46 | t.Log("Initiator ChannelAccount Sequence Number:", initiator.ChannelAccountSequenceNumber) 47 | 48 | // Setup responder. 49 | responder = Participant{ 50 | Name: "Responder", 51 | KP: keypair.MustRandom(), 52 | ChannelAccount: keypair.MustRandom(), 53 | Contribution: 1_000_0000000, 54 | } 55 | t.Log("Responder:", responder.KP.Address()) 56 | t.Log("Responder ChannelAccount:", responder.ChannelAccount.Address()) 57 | { 58 | err := retry(t, 2, func() error { return createAccount(responder.KP.FromAddress()) }) 59 | require.NoError(t, err) 60 | err = retry(t, 2, func() error { 61 | return fundAsset(assetParam.Asset, responder.Contribution, responder.KP, assetParam.Distributor) 62 | }) 63 | require.NoError(t, err) 64 | 65 | t.Log("Responder Contribution:", responder.Contribution, "of asset:", assetParam.Asset.Code(), "issuer: ", assetParam.Asset.Issuer()) 66 | initChannelAccount(t, &responder, assetParam) 67 | } 68 | t.Log("Responder ChannelAccount Sequence Number:", responder.ChannelAccountSequenceNumber) 69 | 70 | return initiator, responder 71 | } 72 | 73 | func initChannelAccount(t *testing.T, participant *Participant, assetParam AssetParam) { 74 | // create channel account 75 | account, err := client.AccountDetail(horizonclient.AccountRequest{AccountID: participant.KP.Address()}) 76 | require.NoError(t, err) 77 | seqNum, err := account.GetSequenceNumber() 78 | require.NoError(t, err) 79 | 80 | tx, err := txbuild.CreateChannelAccount(txbuild.CreateChannelAccountParams{ 81 | Creator: participant.KP.FromAddress(), 82 | ChannelAccount: participant.ChannelAccount.FromAddress(), 83 | SequenceNumber: seqNum + 1, 84 | Asset: assetParam.Asset.Asset(), 85 | }) 86 | require.NoError(t, err) 87 | tx, err = tx.Sign(networkPassphrase, participant.KP, participant.ChannelAccount) 88 | require.NoError(t, err) 89 | fbtx, err := txnbuild.NewFeeBumpTransaction(txnbuild.FeeBumpTransactionParams{ 90 | Inner: tx, 91 | FeeAccount: participant.KP.Address(), 92 | BaseFee: txnbuild.MinBaseFee, 93 | }) 94 | require.NoError(t, err) 95 | fbtx, err = fbtx.Sign(networkPassphrase, participant.KP) 96 | require.NoError(t, err) 97 | var txResp horizon.Transaction 98 | err = retry(t, 2, func() error { 99 | txResp, err = client.SubmitFeeBumpTransaction(fbtx) 100 | return err 101 | }) 102 | require.NoError(t, err) 103 | participant.ChannelAccountSequenceNumber = int64(txResp.Ledger) << 32 104 | 105 | // add initial contribution, use the same contribution for each asset 106 | _, err = account.IncrementSequenceNumber() 107 | require.NoError(t, err) 108 | 109 | payments := []txnbuild.Operation{ 110 | &txnbuild.Payment{ 111 | Destination: participant.ChannelAccount.Address(), 112 | Amount: stellarAmount.StringFromInt64(participant.Contribution), 113 | Asset: assetParam.Asset.Asset(), 114 | }, 115 | } 116 | 117 | tx, err = txnbuild.NewTransaction(txnbuild.TransactionParams{ 118 | SourceAccount: &account, 119 | BaseFee: txnbuild.MinBaseFee, 120 | Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(300)}, 121 | IncrementSequenceNum: true, 122 | Operations: payments, 123 | }) 124 | require.NoError(t, err) 125 | 126 | tx, err = tx.Sign(networkPassphrase, participant.KP) 127 | require.NoError(t, err) 128 | err = retry(t, 2, func() error { 129 | _, err = client.SubmitTransaction(tx) 130 | return err 131 | }) 132 | require.NoError(t, err) 133 | } 134 | 135 | func initChannels(t *testing.T, initiator Participant, responder Participant) (initiatorChannel *state.Channel, responderChannel *state.Channel) { 136 | initiatorChannel = state.NewChannel(state.Config{ 137 | NetworkPassphrase: networkPassphrase, 138 | MaxOpenExpiry: 5 * time.Minute, 139 | Initiator: true, 140 | LocalChannelAccount: initiator.ChannelAccount.FromAddress(), 141 | RemoteChannelAccount: responder.ChannelAccount.FromAddress(), 142 | LocalSigner: initiator.KP, 143 | RemoteSigner: responder.KP.FromAddress(), 144 | }) 145 | responderChannel = state.NewChannel(state.Config{ 146 | NetworkPassphrase: networkPassphrase, 147 | MaxOpenExpiry: 5 * time.Minute, 148 | Initiator: false, 149 | LocalChannelAccount: responder.ChannelAccount.FromAddress(), 150 | RemoteChannelAccount: initiator.ChannelAccount.FromAddress(), 151 | LocalSigner: responder.KP, 152 | RemoteSigner: initiator.KP.FromAddress(), 153 | }) 154 | return initiatorChannel, responderChannel 155 | } 156 | 157 | func initAsset(t *testing.T, client horizonclient.ClientInterface, code string) (state.Asset, *keypair.Full) { 158 | issuerKP := keypair.MustRandom() 159 | distributorKP := keypair.MustRandom() 160 | 161 | err := retry(t, 2, func() error { return createAccount(issuerKP.FromAddress()) }) 162 | require.NoError(t, err) 163 | err = retry(t, 2, func() error { return createAccount(distributorKP.FromAddress()) }) 164 | require.NoError(t, err) 165 | 166 | var distributor horizon.Account 167 | err = retry(t, 2, func() error { 168 | distributor, err = client.AccountDetail(horizonclient.AccountRequest{AccountID: distributorKP.Address()}) 169 | return err 170 | }) 171 | require.NoError(t, err) 172 | 173 | asset := txnbuild.CreditAsset{Code: code, Issuer: issuerKP.Address()} 174 | 175 | tx, err := txnbuild.NewTransaction( 176 | txnbuild.TransactionParams{ 177 | SourceAccount: &distributor, 178 | IncrementSequenceNum: true, 179 | BaseFee: txnbuild.MinBaseFee, 180 | Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewInfiniteTimeout()}, 181 | Operations: []txnbuild.Operation{ 182 | &txnbuild.ChangeTrust{ 183 | Line: asset.MustToChangeTrustAsset(), 184 | Limit: "5000", 185 | }, 186 | &txnbuild.Payment{ 187 | Destination: distributorKP.Address(), 188 | Asset: asset, 189 | Amount: "5000", 190 | SourceAccount: issuerKP.Address(), 191 | }, 192 | }, 193 | }, 194 | ) 195 | require.NoError(t, err) 196 | tx, err = tx.Sign(networkPassphrase, distributorKP, issuerKP) 197 | require.NoError(t, err) 198 | err = retry(t, 2, func() error { 199 | _, err = client.SubmitTransaction(tx) 200 | return err 201 | }) 202 | require.NoError(t, err) 203 | 204 | return state.Asset(asset.Code + ":" + asset.Issuer), distributorKP 205 | } 206 | 207 | func randomBool(t *testing.T) bool { 208 | t.Helper() 209 | b := [1]byte{} 210 | _, err := rand.Read(b[:]) 211 | require.NoError(t, err) 212 | return b[0]%2 == 0 213 | } 214 | 215 | func randomPositiveInt64(t *testing.T, max int64) int64 { 216 | t.Helper() 217 | var i uint32 218 | err := binary.Read(rand.Reader, binary.LittleEndian, &i) 219 | require.NoError(t, err) 220 | return int64(i) % max 221 | } 222 | 223 | func retry(t *testing.T, maxAttempts int, f func() error) (err error) { 224 | t.Helper() 225 | for i := 0; i < maxAttempts; i++ { 226 | err = f() 227 | if err == nil { 228 | return 229 | } 230 | t.Logf("failed attempt %d at performing a retry-able operation: %v", i, err) 231 | time.Sleep(2 * time.Second) 232 | } 233 | return err 234 | } 235 | 236 | func fundAsset(asset state.Asset, amount int64, accountKP *keypair.Full, distributorKP *keypair.Full) error { 237 | distributor, err := client.AccountDetail(horizonclient.AccountRequest{AccountID: distributorKP.Address()}) 238 | if err != nil { 239 | return err 240 | } 241 | 242 | ops := []txnbuild.Operation{} 243 | if !asset.IsNative() { 244 | ops = append(ops, &txnbuild.ChangeTrust{ 245 | SourceAccount: accountKP.Address(), 246 | Line: asset.Asset().MustToChangeTrustAsset(), 247 | Limit: "5000", 248 | }) 249 | } 250 | ops = append(ops, &txnbuild.Payment{ 251 | Destination: accountKP.Address(), 252 | Amount: stellarAmount.StringFromInt64(amount), 253 | Asset: asset.Asset(), 254 | }) 255 | tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ 256 | SourceAccount: &distributor, 257 | IncrementSequenceNum: true, 258 | BaseFee: txnbuild.MinBaseFee, 259 | Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(300)}, 260 | Operations: ops, 261 | }) 262 | if err != nil { 263 | return err 264 | } 265 | if !asset.IsNative() { 266 | tx, err = tx.Sign(networkPassphrase, accountKP) 267 | if err != nil { 268 | return err 269 | } 270 | } 271 | tx, err = tx.Sign(networkPassphrase, distributorKP) 272 | if err != nil { 273 | return err 274 | } 275 | _, err = client.SubmitTransaction(tx) 276 | if err != nil { 277 | return err 278 | } 279 | return nil 280 | } 281 | 282 | func createAccount(account *keypair.FromAddress) error { 283 | // Try to fund via the fund endpoint first. 284 | _, err := client.Fund(account.Address()) 285 | if err == nil { 286 | return nil 287 | } 288 | 289 | // Otherwise, fund via the root account. 290 | startingBalance := int64(1_000_0000000) 291 | rootResp, err := client.Root() 292 | if err != nil { 293 | return err 294 | } 295 | root := keypair.Master(rootResp.NetworkPassphrase).(*keypair.Full) 296 | sourceAccount, err := client.AccountDetail(horizonclient.AccountRequest{AccountID: root.Address()}) 297 | if err != nil { 298 | return err 299 | } 300 | tx, err := txnbuild.NewTransaction( 301 | txnbuild.TransactionParams{ 302 | SourceAccount: &sourceAccount, 303 | IncrementSequenceNum: true, 304 | BaseFee: txnbuild.MinBaseFee, 305 | Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(300)}, 306 | Operations: []txnbuild.Operation{ 307 | &txnbuild.CreateAccount{ 308 | Destination: account.Address(), 309 | Amount: stellarAmount.StringFromInt64(startingBalance), 310 | }, 311 | }, 312 | }, 313 | ) 314 | if err != nil { 315 | return err 316 | } 317 | tx, err = tx.Sign(rootResp.NetworkPassphrase, root) 318 | if err != nil { 319 | return err 320 | } 321 | _, err = client.SubmitTransaction(tx) 322 | if err != nil { 323 | return err 324 | } 325 | return nil 326 | } 327 | 328 | func txSeqs(txs []*txnbuild.Transaction) []int64 { 329 | seqs := make([]int64, len(txs)) 330 | for i := range txs { 331 | seqs[i] = txs[i].SequenceNumber() 332 | } 333 | return seqs 334 | } 335 | 336 | func assetBalance(asset state.Asset, account horizon.Account) string { 337 | for _, b := range account.Balances { 338 | if b.Asset.Code == asset.Code() { 339 | return b.Balance 340 | } 341 | } 342 | return "0" 343 | } 344 | -------------------------------------------------------------------------------- /sdk/state/integrationtests/main_test.go: -------------------------------------------------------------------------------- 1 | package integrationtests 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stellar/go/clients/horizonclient" 11 | ) 12 | 13 | var horizonURL = func() string { 14 | url := os.Getenv("INTEGRATION_TESTS_HORIZON_URL") 15 | if url == "" { 16 | url = "https://horizon-testnet.stellar.org" 17 | } 18 | return url 19 | }() 20 | 21 | var ( 22 | networkPassphrase string 23 | client *horizonclient.Client 24 | ) 25 | 26 | func TestMain(m *testing.M) { 27 | if os.Getenv("INTEGRATION_TESTS") == "" { 28 | fmt.Fprintln(os.Stderr, "SKIP") 29 | os.Exit(0) 30 | } 31 | 32 | client = &horizonclient.Client{ 33 | HorizonURL: horizonURL, 34 | HTTP: &http.Client{ 35 | Timeout: 20 * time.Second, 36 | }, 37 | } 38 | networkDetails, err := client.Root() 39 | if err != nil { 40 | fmt.Fprintf(os.Stderr, "error: %v\n", err) 41 | os.Exit(1) 42 | } 43 | networkPassphrase = networkDetails.NetworkPassphrase 44 | 45 | os.Exit(m.Run()) 46 | } 47 | -------------------------------------------------------------------------------- /sdk/state/state.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/stellar/go/keypair" 8 | "github.com/stellar/starlight/sdk/txbuild" 9 | ) 10 | 11 | // Config contains the information for setting up a new channel. 12 | type Config struct { 13 | NetworkPassphrase string 14 | MaxOpenExpiry time.Duration 15 | 16 | Initiator bool 17 | 18 | LocalChannelAccount *keypair.FromAddress 19 | RemoteChannelAccount *keypair.FromAddress 20 | 21 | LocalSigner *keypair.Full 22 | RemoteSigner *keypair.FromAddress 23 | } 24 | 25 | // NewChannel constructs a new channel with the given config. 26 | func NewChannel(c Config) *Channel { 27 | channel := &Channel{ 28 | networkPassphrase: c.NetworkPassphrase, 29 | maxOpenExpiry: c.MaxOpenExpiry, 30 | initiator: c.Initiator, 31 | localChannelAccount: &ChannelAccount{Address: c.LocalChannelAccount}, 32 | remoteChannelAccount: &ChannelAccount{Address: c.RemoteChannelAccount}, 33 | localSigner: c.LocalSigner, 34 | remoteSigner: c.RemoteSigner, 35 | } 36 | return channel 37 | } 38 | 39 | // Snapshot is a snapshot of a Channel's internal state. If a Snapshot is 40 | // combined with a Channel's initialization config they can be used to create a 41 | // new Channel that has the same state. 42 | type Snapshot struct { 43 | LocalChannelAccountSequence int64 44 | LocalChannelAccountBalance int64 45 | LocalChannelAccountLastSeenTransactionOrderID int64 46 | RemoteChannelAccountSequence int64 47 | RemoteChannelAccountBalance int64 48 | RemoteChannelAccountLastSeenTransactionOrderID int64 49 | 50 | OpenAgreement OpenAgreement 51 | OpenExecutedAndValidated bool 52 | OpenExecutedWithError bool 53 | 54 | LatestAuthorizedCloseAgreement CloseAgreement 55 | LatestUnauthorizedCloseAgreement CloseAgreement 56 | } 57 | 58 | // NewChannelFromSnapshot creates the channel with the given config, and 59 | // restores the internal state of the channel using the snapshot. To restore the 60 | // channel to its identical state the same config should be provided that was in 61 | // use when the snapshot was created. 62 | func NewChannelFromSnapshot(c Config, s Snapshot) *Channel { 63 | channel := NewChannel(c) 64 | 65 | channel.localChannelAccount.SequenceNumber = s.LocalChannelAccountSequence 66 | channel.localChannelAccount.Balance = s.LocalChannelAccountBalance 67 | channel.localChannelAccount.LastSeenTransactionOrderID = s.LocalChannelAccountLastSeenTransactionOrderID 68 | channel.remoteChannelAccount.SequenceNumber = s.RemoteChannelAccountSequence 69 | channel.remoteChannelAccount.Balance = s.RemoteChannelAccountBalance 70 | channel.remoteChannelAccount.LastSeenTransactionOrderID = s.RemoteChannelAccountLastSeenTransactionOrderID 71 | 72 | channel.openAgreement = s.OpenAgreement 73 | channel.openExecutedAndValidated = s.OpenExecutedAndValidated 74 | if s.OpenExecutedWithError { 75 | channel.openExecutedWithError = fmt.Errorf("open executed with error") 76 | } 77 | 78 | channel.latestAuthorizedCloseAgreement = s.LatestAuthorizedCloseAgreement 79 | channel.latestUnauthorizedCloseAgreement = s.LatestUnauthorizedCloseAgreement 80 | 81 | return channel 82 | } 83 | 84 | // ChannelAccount holds the details that a Channel tracks for a Stellar account. 85 | // A Channel tracks the details of two Stellar accounts, one for each 86 | // participant in the channel. 87 | // 88 | // The channel accounts hold the assets that are used for payments and will be 89 | // re-distributed at close. 90 | type ChannelAccount struct { 91 | Address *keypair.FromAddress 92 | SequenceNumber int64 93 | Balance int64 94 | LastSeenTransactionOrderID int64 95 | } 96 | 97 | // Channel holds the state of a single Starlight payment channel. 98 | type Channel struct { 99 | networkPassphrase string 100 | maxOpenExpiry time.Duration 101 | 102 | initiator bool 103 | localChannelAccount *ChannelAccount 104 | remoteChannelAccount *ChannelAccount 105 | 106 | localSigner *keypair.Full 107 | remoteSigner *keypair.FromAddress 108 | 109 | openAgreement OpenAgreement 110 | openExecutedAndValidated bool 111 | openExecutedWithError error 112 | 113 | latestAuthorizedCloseAgreement CloseAgreement 114 | latestUnauthorizedCloseAgreement CloseAgreement 115 | } 116 | 117 | // Snapshot returns a snapshot of the channel's internal state that if combined 118 | // with it's initialization config can be used to create a new Channel that has 119 | // the same state. 120 | func (c *Channel) Snapshot() Snapshot { 121 | return Snapshot{ 122 | LocalChannelAccountSequence: c.localChannelAccount.SequenceNumber, 123 | LocalChannelAccountBalance: c.localChannelAccount.Balance, 124 | LocalChannelAccountLastSeenTransactionOrderID: c.localChannelAccount.LastSeenTransactionOrderID, 125 | RemoteChannelAccountSequence: c.remoteChannelAccount.SequenceNumber, 126 | RemoteChannelAccountBalance: c.remoteChannelAccount.Balance, 127 | RemoteChannelAccountLastSeenTransactionOrderID: c.remoteChannelAccount.LastSeenTransactionOrderID, 128 | 129 | OpenAgreement: c.openAgreement, 130 | OpenExecutedAndValidated: c.openExecutedAndValidated, 131 | OpenExecutedWithError: c.openExecutedWithError != nil, 132 | 133 | LatestAuthorizedCloseAgreement: c.latestAuthorizedCloseAgreement, 134 | LatestUnauthorizedCloseAgreement: c.latestUnauthorizedCloseAgreement, 135 | } 136 | } 137 | 138 | type State int 139 | 140 | const ( 141 | StateError State = iota - 1 142 | StateNone 143 | StateOpen 144 | StateClosingWithOutdatedState 145 | StateClosedWithOutdatedState 146 | StateClosing 147 | StateClosed 148 | ) 149 | 150 | // State returns a single value representing the overall state of the 151 | // channel. If there was an error finding the state, or internal values are 152 | // unexpected, then a failed channel state is returned, indicating something is 153 | // wrong. 154 | func (c *Channel) State() (State, error) { 155 | if c.openExecutedWithError != nil { 156 | return StateError, nil 157 | } 158 | 159 | if !c.openExecutedAndValidated { 160 | return StateNone, nil 161 | } 162 | 163 | // Get the sequence numbers for the latest close agreement transactions. 164 | txs, err := c.closeTxs(c.openAgreement.Envelope.Details, c.latestAuthorizedCloseAgreement.Envelope.Details) 165 | if err != nil { 166 | return -1, fmt.Errorf("building declaration and close txs for latest authorized close agreement: %w", err) 167 | } 168 | latestDeclSequence := txs.Declaration.SequenceNumber() 169 | latestCloseSequence := txs.Close.SequenceNumber() 170 | 171 | initiatorChannelAccountSeqNum := c.initiatorChannelAccount().SequenceNumber 172 | s := c.openAgreement.Envelope.Details.StartingSequence 173 | 174 | if initiatorChannelAccountSeqNum == s { 175 | return StateOpen, nil 176 | } else if initiatorChannelAccountSeqNum < latestDeclSequence && 177 | txbuild.SequenceNumberToTransactionType(s, initiatorChannelAccountSeqNum) == txbuild.TransactionTypeDeclaration { 178 | return StateClosingWithOutdatedState, nil 179 | } else if initiatorChannelAccountSeqNum < latestDeclSequence && 180 | txbuild.SequenceNumberToTransactionType(s, initiatorChannelAccountSeqNum) == txbuild.TransactionTypeClose { 181 | return StateClosedWithOutdatedState, nil 182 | } else if initiatorChannelAccountSeqNum == latestDeclSequence { 183 | return StateClosing, nil 184 | } else if initiatorChannelAccountSeqNum >= latestCloseSequence { 185 | return StateClosed, nil 186 | } 187 | 188 | return StateError, fmt.Errorf("initiator channel account sequence has unexpected value") 189 | } 190 | 191 | func (c *Channel) setInitiatorChannelAccountSequence(seqNum int64) { 192 | c.initiatorChannelAccount().SequenceNumber = seqNum 193 | } 194 | 195 | // IsInitiator returns true if this channel initiated the process of opening a 196 | // channel, else false. 197 | func (c *Channel) IsInitiator() bool { 198 | return c.initiator 199 | } 200 | 201 | // nextIterationNumber returns the next iteration number for the channel. If 202 | // there is a pending unauthorized close agreement, then that agreement 203 | // iteration is used, else the latest authorized agreeement is used. 204 | func (c *Channel) nextIterationNumber() int64 { 205 | if !c.latestUnauthorizedCloseAgreement.Envelope.Empty() { 206 | return c.latestUnauthorizedCloseAgreement.Envelope.Details.IterationNumber 207 | } 208 | return c.latestAuthorizedCloseAgreement.Envelope.Details.IterationNumber + 1 209 | } 210 | 211 | // Balance returns the amount owing from the initiator to the responder, if positive, or 212 | // the amount owing from the responder to the initiator, if negative. 213 | func (c *Channel) Balance() int64 { 214 | return c.latestAuthorizedCloseAgreement.Envelope.Details.Balance 215 | } 216 | 217 | // OpenAgreement returns the open agreement used to open the channel. 218 | func (c *Channel) OpenAgreement() OpenAgreement { 219 | return c.openAgreement 220 | } 221 | 222 | // LatestCloseAgreement returns the latest close agreement signed by both 223 | // channel participants. 224 | func (c *Channel) LatestCloseAgreement() CloseAgreement { 225 | return c.latestAuthorizedCloseAgreement 226 | } 227 | 228 | // LatestUnauthorizedCloseAgreement returns the latest unauthorized close 229 | // agreement yet to be signed by both participants. 230 | func (c *Channel) LatestUnauthorizedCloseAgreement() (CloseAgreement, bool) { 231 | return c.latestUnauthorizedCloseAgreement, !c.latestUnauthorizedCloseAgreement.Envelope.Empty() 232 | } 233 | 234 | // UpdateLocalChannelAccountBalance updates the local channel account balance. 235 | func (c *Channel) UpdateLocalChannelAccountBalance(balance int64) { 236 | c.localChannelAccount.Balance = balance 237 | } 238 | 239 | // UpdateRemoteChannelAccountBalance updates the remote channel account balance. 240 | func (c *Channel) UpdateRemoteChannelAccountBalance(balance int64) { 241 | c.remoteChannelAccount.Balance = balance 242 | } 243 | 244 | // LocalChannelAccount returns the local channel account. 245 | func (c *Channel) LocalChannelAccount() ChannelAccount { 246 | return *c.localChannelAccount 247 | } 248 | 249 | // RemoteChannelAccount returns the remote channel account. 250 | func (c *Channel) RemoteChannelAccount() ChannelAccount { 251 | return *c.remoteChannelAccount 252 | } 253 | 254 | func (c *Channel) initiatorChannelAccount() *ChannelAccount { 255 | if c.initiator { 256 | return c.localChannelAccount 257 | } else { 258 | return c.remoteChannelAccount 259 | } 260 | } 261 | 262 | func (c *Channel) responderChannelAccount() *ChannelAccount { 263 | if c.initiator { 264 | return c.remoteChannelAccount 265 | } else { 266 | return c.localChannelAccount 267 | } 268 | } 269 | 270 | func (c *Channel) initiatorSigner() *keypair.FromAddress { 271 | if c.initiator { 272 | return c.localSigner.FromAddress() 273 | } else { 274 | return c.remoteSigner 275 | } 276 | } 277 | 278 | func (c *Channel) responderSigner() *keypair.FromAddress { 279 | if c.initiator { 280 | return c.remoteSigner 281 | } else { 282 | return c.localSigner.FromAddress() 283 | } 284 | } 285 | 286 | func (c *Channel) amountToLocal(balance int64) int64 { 287 | if c.initiator { 288 | return amountToInitiator(balance) 289 | } 290 | return amountToResponder(balance) 291 | } 292 | 293 | func (c *Channel) amountToRemote(balance int64) int64 { 294 | if c.initiator { 295 | return amountToResponder(balance) 296 | } 297 | return amountToInitiator(balance) 298 | } 299 | 300 | func amountToInitiator(balance int64) int64 { 301 | if balance < 0 { 302 | return balance * -1 303 | } 304 | return 0 305 | } 306 | 307 | func amountToResponder(balance int64) int64 { 308 | if balance > 0 { 309 | return balance 310 | } 311 | return 0 312 | } 313 | -------------------------------------------------------------------------------- /sdk/state/state_test.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stellar/go/keypair" 9 | "github.com/stellar/go/network" 10 | "github.com/stellar/go/txnbuild" 11 | "github.com/stellar/go/xdr" 12 | "github.com/stellar/starlight/sdk/txbuild/txbuildtest" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func assertChannelSnapshotsAndRestores(t *testing.T, config Config, channel *Channel) { 18 | t.Helper() 19 | 20 | snapshot := channel.Snapshot() 21 | snapshotJSON, err := json.Marshal(snapshot) 22 | require.NoError(t, err) 23 | restoredSnapshot := Snapshot{} 24 | err = json.Unmarshal(snapshotJSON, &restoredSnapshot) 25 | require.NoError(t, err) 26 | 27 | restoredChannel := NewChannelFromSnapshot(config, snapshot) 28 | 29 | require.Equal(t, channel, restoredChannel) 30 | } 31 | 32 | func TestNewChannelWithSnapshot(t *testing.T) { 33 | localSigner := keypair.MustRandom() 34 | remoteSigner := keypair.MustRandom() 35 | localChannelAccount := keypair.MustRandom().FromAddress() 36 | remoteChannelAccount := keypair.MustRandom().FromAddress() 37 | 38 | localConfig := Config{ 39 | NetworkPassphrase: network.TestNetworkPassphrase, 40 | Initiator: true, 41 | LocalSigner: localSigner, 42 | RemoteSigner: remoteSigner.FromAddress(), 43 | LocalChannelAccount: localChannelAccount, 44 | RemoteChannelAccount: remoteChannelAccount, 45 | MaxOpenExpiry: 2 * time.Hour, 46 | } 47 | localChannel := NewChannel(localConfig) 48 | remoteConfig := Config{ 49 | NetworkPassphrase: network.TestNetworkPassphrase, 50 | Initiator: false, 51 | LocalSigner: remoteSigner, 52 | RemoteSigner: localSigner.FromAddress(), 53 | LocalChannelAccount: remoteChannelAccount, 54 | RemoteChannelAccount: localChannelAccount, 55 | MaxOpenExpiry: 2 * time.Hour, 56 | } 57 | remoteChannel := NewChannel(remoteConfig) 58 | 59 | // Check snapshot rehydrates the channel identically when new and not open. 60 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel) 61 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel) 62 | 63 | // Negotiate the open state. 64 | { 65 | open1, err := localChannel.ProposeOpen(OpenParams{ 66 | ObservationPeriodTime: 1, 67 | ObservationPeriodLedgerGap: 1, 68 | ExpiresAt: time.Now().Add(time.Hour), 69 | StartingSequence: 101, 70 | }) 71 | require.NoError(t, err) 72 | open2, err := remoteChannel.ConfirmOpen(open1.Envelope) 73 | require.NoError(t, err) 74 | _, err = localChannel.ConfirmOpen(open2.Envelope) 75 | require.NoError(t, err) 76 | } 77 | 78 | // Check snapshot rehydrates the channel identically when open. 79 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel) 80 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel) 81 | 82 | // Put the channel into an open state by ingesting the open tx. 83 | { 84 | ftx, err := localChannel.OpenTx() 85 | require.NoError(t, err) 86 | ftxXDR, err := ftx.Base64() 87 | require.NoError(t, err) 88 | 89 | successResultXDR, err := txbuildtest.BuildResultXDR(true) 90 | require.NoError(t, err) 91 | resultMetaXDR, err := txbuildtest.BuildOpenResultMetaXDR(txbuildtest.OpenResultMetaParams{ 92 | InitiatorSigner: localSigner.Address(), 93 | ResponderSigner: remoteSigner.Address(), 94 | InitiatorChannelAccount: localChannelAccount.Address(), 95 | ResponderChannelAccount: remoteChannelAccount.Address(), 96 | StartSequence: 101, 97 | Asset: txnbuild.NativeAsset{}, 98 | }) 99 | require.NoError(t, err) 100 | 101 | err = localChannel.IngestTx(1, ftxXDR, successResultXDR, resultMetaXDR) 102 | require.NoError(t, err) 103 | err = remoteChannel.IngestTx(1, ftxXDR, successResultXDR, resultMetaXDR) 104 | require.NoError(t, err) 105 | 106 | cs, err := localChannel.State() 107 | require.NoError(t, err) 108 | assert.Equal(t, StateOpen, cs) 109 | 110 | cs, err = remoteChannel.State() 111 | require.NoError(t, err) 112 | assert.Equal(t, StateOpen, cs) 113 | } 114 | 115 | // Check snapshot rehydrates the channel identically when open. 116 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel) 117 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel) 118 | 119 | // Update balances. 120 | localChannel.UpdateLocalChannelAccountBalance(100) 121 | localChannel.UpdateRemoteChannelAccountBalance(200) 122 | remoteChannel.UpdateLocalChannelAccountBalance(300) 123 | remoteChannel.UpdateRemoteChannelAccountBalance(400) 124 | 125 | // Check snapshot rehydrates the channel identically when open and with 126 | // balances updated. 127 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel) 128 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel) 129 | 130 | // Propose a payment. 131 | ca, err := localChannel.ProposePayment(10) 132 | require.NoError(t, err) 133 | 134 | // Check snapshot rehydrates the channel identically when payment proposed. 135 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel) 136 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel) 137 | 138 | // Confirm the payment. 139 | ca, err = remoteChannel.ConfirmPayment(ca.Envelope) 140 | require.NoError(t, err) 141 | _, err = localChannel.ConfirmPayment(ca.Envelope) 142 | require.NoError(t, err) 143 | 144 | // Check snapshot rehydrates the channel identically when payment confirmed. 145 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel) 146 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel) 147 | 148 | // Propose a close. 149 | ca, err = localChannel.ProposeClose() 150 | require.NoError(t, err) 151 | 152 | // Check snapshot rehydrates the channel identically when payment confirmed. 153 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel) 154 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel) 155 | 156 | // Put the channel into a closing state by ingesting the closing declaration tx. 157 | { 158 | dtx, _, err := localChannel.CloseTxs() 159 | require.NoError(t, err) 160 | dtxXDR, err := dtx.Base64() 161 | require.NoError(t, err) 162 | 163 | successResultXDR, err := txbuildtest.BuildResultXDR(true) 164 | require.NoError(t, err) 165 | 166 | resultMetaXDR, err := txbuildtest.BuildResultMetaXDR([]xdr.LedgerEntryData{ 167 | { 168 | Type: xdr.LedgerEntryTypeAccount, 169 | Account: &xdr.AccountEntry{ 170 | AccountId: xdr.MustAddress(localChannelAccount.Address()), 171 | SeqNum: 102, 172 | Signers: []xdr.Signer{ 173 | {Key: xdr.MustSigner(localSigner.Address()), Weight: 1}, 174 | {Key: xdr.MustSigner(remoteSigner.Address()), Weight: 1}, 175 | }, 176 | Thresholds: xdr.Thresholds{0, 2, 2, 2}, 177 | }, 178 | }, 179 | { 180 | Type: xdr.LedgerEntryTypeAccount, 181 | Account: &xdr.AccountEntry{ 182 | AccountId: xdr.MustAddress(remoteChannelAccount.Address()), 183 | SeqNum: 103, 184 | Signers: []xdr.Signer{ 185 | {Key: xdr.MustSigner(remoteSigner.Address()), Weight: 1}, 186 | {Key: xdr.MustSigner(localSigner.Address()), Weight: 1}, 187 | }, 188 | Thresholds: xdr.Thresholds{0, 2, 2, 2}, 189 | }, 190 | }, 191 | }) 192 | require.NoError(t, err) 193 | 194 | err = localChannel.IngestTx(1, dtxXDR, successResultXDR, resultMetaXDR) 195 | require.NoError(t, err) 196 | err = remoteChannel.IngestTx(1, dtxXDR, successResultXDR, resultMetaXDR) 197 | require.NoError(t, err) 198 | 199 | cs, err := localChannel.State() 200 | require.NoError(t, err) 201 | assert.Equal(t, StateClosing, cs) 202 | 203 | cs, err = remoteChannel.State() 204 | require.NoError(t, err) 205 | assert.Equal(t, StateClosing, cs) 206 | } 207 | 208 | // Check snapshot rehydrates the channel identically when closing ingested. 209 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel) 210 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel) 211 | 212 | // Confirm the close. 213 | ca, err = remoteChannel.ConfirmClose(ca.Envelope) 214 | require.NoError(t, err) 215 | _, err = localChannel.ConfirmClose(ca.Envelope) 216 | require.NoError(t, err) 217 | 218 | // Check snapshot rehydrates the channel identically when payment confirmed. 219 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel) 220 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel) 221 | 222 | // Put the channel into a close state by ingesting the close tx. 223 | { 224 | _, ctx, err := localChannel.CloseTxs() 225 | require.NoError(t, err) 226 | ctxXDR, err := ctx.Base64() 227 | require.NoError(t, err) 228 | 229 | successResultXDR, err := txbuildtest.BuildResultXDR(true) 230 | require.NoError(t, err) 231 | 232 | resultMetaXDR, err := txbuildtest.BuildResultMetaXDR([]xdr.LedgerEntryData{}) 233 | require.NoError(t, err) 234 | 235 | err = localChannel.IngestTx(1, ctxXDR, successResultXDR, resultMetaXDR) 236 | require.NoError(t, err) 237 | err = remoteChannel.IngestTx(1, ctxXDR, successResultXDR, resultMetaXDR) 238 | require.NoError(t, err) 239 | 240 | cs, err := localChannel.State() 241 | require.NoError(t, err) 242 | assert.Equal(t, StateClosed, cs) 243 | 244 | cs, err = remoteChannel.State() 245 | require.NoError(t, err) 246 | assert.Equal(t, StateClosed, cs) 247 | } 248 | 249 | // Check snapshot rehydrates the channel identically when payment confirmed. 250 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel) 251 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel) 252 | } 253 | -------------------------------------------------------------------------------- /sdk/state/verify.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "github.com/stellar/go/keypair" 5 | "github.com/stellar/go/xdr" 6 | "golang.org/x/sync/errgroup" 7 | ) 8 | 9 | type signatureVerificationInput struct { 10 | TransactionHash TransactionHash 11 | Signature xdr.Signature 12 | Signer *keypair.FromAddress 13 | } 14 | 15 | func verifySignatures(inputs []signatureVerificationInput) error { 16 | g := errgroup.Group{} 17 | for _, i := range inputs { 18 | i := i 19 | g.Go(func() error { 20 | return i.Signer.Verify(i.TransactionHash[:], []byte(i.Signature)) 21 | }) 22 | } 23 | return g.Wait() 24 | } 25 | -------------------------------------------------------------------------------- /sdk/txbuild/close.go: -------------------------------------------------------------------------------- 1 | package txbuild 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/stellar/go/amount" 8 | "github.com/stellar/go/keypair" 9 | "github.com/stellar/go/txnbuild" 10 | ) 11 | 12 | type CloseParams struct { 13 | ObservationPeriodTime time.Duration 14 | ObservationPeriodLedgerGap uint32 15 | InitiatorSigner *keypair.FromAddress 16 | ResponderSigner *keypair.FromAddress 17 | InitiatorChannelAccount *keypair.FromAddress 18 | ResponderChannelAccount *keypair.FromAddress 19 | StartSequence int64 20 | IterationNumber int64 21 | AmountToInitiator int64 22 | AmountToResponder int64 23 | Asset txnbuild.Asset 24 | } 25 | 26 | func Close(p CloseParams) (*txnbuild.Transaction, error) { 27 | if p.IterationNumber < 0 || p.StartSequence <= 0 { 28 | return nil, fmt.Errorf("invalid iteration number or start sequence: cannot be negative") 29 | } 30 | 31 | // Close is the second transaction in an iteration's transaction set. 32 | seq := startSequenceOfIteration(p.StartSequence, p.IterationNumber) + 1 33 | if seq < 0 { 34 | return nil, fmt.Errorf("invalid sequence number: cannot be negative") 35 | } 36 | 37 | tp := txnbuild.TransactionParams{ 38 | SourceAccount: &txnbuild.SimpleAccount{ 39 | AccountID: p.InitiatorChannelAccount.Address(), 40 | Sequence: seq, 41 | }, 42 | BaseFee: 0, 43 | Preconditions: txnbuild.Preconditions{ 44 | TimeBounds: txnbuild.NewInfiniteTimeout(), 45 | MinSequenceNumberAge: uint64(p.ObservationPeriodTime.Seconds()), 46 | MinSequenceNumberLedgerGap: p.ObservationPeriodLedgerGap, 47 | }, 48 | Operations: []txnbuild.Operation{ 49 | &txnbuild.SetOptions{ 50 | SourceAccount: p.InitiatorChannelAccount.Address(), 51 | MasterWeight: txnbuild.NewThreshold(0), 52 | LowThreshold: txnbuild.NewThreshold(1), 53 | MediumThreshold: txnbuild.NewThreshold(1), 54 | HighThreshold: txnbuild.NewThreshold(1), 55 | Signer: &txnbuild.Signer{Address: p.ResponderSigner.Address(), Weight: 0}, 56 | }, 57 | &txnbuild.SetOptions{ 58 | SourceAccount: p.ResponderChannelAccount.Address(), 59 | MasterWeight: txnbuild.NewThreshold(0), 60 | LowThreshold: txnbuild.NewThreshold(1), 61 | MediumThreshold: txnbuild.NewThreshold(1), 62 | HighThreshold: txnbuild.NewThreshold(1), 63 | Signer: &txnbuild.Signer{Address: p.InitiatorSigner.Address(), Weight: 0}, 64 | }, 65 | }, 66 | } 67 | if p.AmountToInitiator != 0 { 68 | tp.Operations = append(tp.Operations, &txnbuild.Payment{ 69 | SourceAccount: p.ResponderChannelAccount.Address(), 70 | Destination: p.InitiatorChannelAccount.Address(), 71 | Asset: p.Asset, 72 | Amount: amount.StringFromInt64(p.AmountToInitiator), 73 | }) 74 | } 75 | if p.AmountToResponder != 0 { 76 | tp.Operations = append(tp.Operations, &txnbuild.Payment{ 77 | SourceAccount: p.InitiatorChannelAccount.Address(), 78 | Destination: p.ResponderChannelAccount.Address(), 79 | Asset: p.Asset, 80 | Amount: amount.StringFromInt64(p.AmountToResponder), 81 | }) 82 | } 83 | tx, err := txnbuild.NewTransaction(tp) 84 | if err != nil { 85 | return nil, err 86 | } 87 | return tx, nil 88 | } 89 | -------------------------------------------------------------------------------- /sdk/txbuild/close_test.go: -------------------------------------------------------------------------------- 1 | package txbuild 2 | 3 | import ( 4 | "encoding/base64" 5 | "math" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stellar/go/keypair" 10 | "github.com/stellar/go/txnbuild" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestClose_size(t *testing.T) { 16 | initiatorSigner := keypair.MustRandom() 17 | responderSigner := keypair.MustRandom() 18 | initiatorChannelAccount := keypair.MustRandom() 19 | responderChannelAccount := keypair.MustRandom() 20 | 21 | tx, err := Close(CloseParams{ 22 | ObservationPeriodTime: 5 * time.Minute, 23 | ObservationPeriodLedgerGap: 5, 24 | InitiatorSigner: initiatorSigner.FromAddress(), 25 | ResponderSigner: responderSigner.FromAddress(), 26 | InitiatorChannelAccount: initiatorChannelAccount.FromAddress(), 27 | ResponderChannelAccount: responderChannelAccount.FromAddress(), 28 | StartSequence: 101, 29 | IterationNumber: 1, 30 | AmountToInitiator: 100, 31 | AmountToResponder: 200, 32 | Asset: txnbuild.CreditAsset{Code: "ETH", Issuer: "GBTYEE5BTST64JCBUXVAEEPQJAY3TNV47A5JFUMQKNDWUJRRT6LUVEQH"}, 33 | }) 34 | require.NoError(t, err) 35 | 36 | // Test the size without signers. 37 | { 38 | txb, err := tx.MarshalBinary() 39 | require.NoError(t, err) 40 | t.Log("unsigned:", base64.StdEncoding.EncodeToString(txb)) 41 | assert.Len(t, txb, 652) 42 | } 43 | 44 | // Test the size with signers. 45 | { 46 | tx, err := tx.Sign("test", initiatorSigner, responderSigner) 47 | require.NoError(t, err) 48 | txb, err := tx.MarshalBinary() 49 | require.NoError(t, err) 50 | t.Log("signed:", base64.StdEncoding.EncodeToString(txb)) 51 | assert.Len(t, txb, 796) 52 | } 53 | } 54 | 55 | func TestClose_iterationNumber_checkNonNegative(t *testing.T) { 56 | _, err := Close(CloseParams{ 57 | StartSequence: 101, 58 | IterationNumber: -1, 59 | }) 60 | assert.EqualError(t, err, "invalid iteration number or start sequence: cannot be negative") 61 | _, err = Close(CloseParams{ 62 | StartSequence: -1, 63 | IterationNumber: 5, 64 | }) 65 | assert.EqualError(t, err, "invalid iteration number or start sequence: cannot be negative") 66 | } 67 | 68 | func TestClose_startSequenceOfIteration_checkNonNegative(t *testing.T) { 69 | _, err := Close(CloseParams{ 70 | IterationNumber: 0, 71 | StartSequence: math.MaxInt64, 72 | }) 73 | assert.EqualError(t, err, "invalid sequence number: cannot be negative") 74 | } 75 | -------------------------------------------------------------------------------- /sdk/txbuild/create_channelaccount.go: -------------------------------------------------------------------------------- 1 | package txbuild 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/stellar/go/amount" 7 | "github.com/stellar/go/keypair" 8 | "github.com/stellar/go/txnbuild" 9 | ) 10 | 11 | type CreateChannelAccountParams struct { 12 | Creator *keypair.FromAddress 13 | ChannelAccount *keypair.FromAddress 14 | SequenceNumber int64 15 | Asset txnbuild.BasicAsset 16 | } 17 | 18 | func CreateChannelAccount(p CreateChannelAccountParams) (*txnbuild.Transaction, error) { 19 | ops := []txnbuild.Operation{ 20 | &txnbuild.BeginSponsoringFutureReserves{ 21 | SponsoredID: p.ChannelAccount.Address(), 22 | }, 23 | &txnbuild.CreateAccount{ 24 | Destination: p.ChannelAccount.Address(), 25 | // base reserves sponsored by p.Creator 26 | Amount: "0", 27 | }, 28 | &txnbuild.SetOptions{ 29 | SourceAccount: p.ChannelAccount.Address(), 30 | MasterWeight: txnbuild.NewThreshold(0), 31 | LowThreshold: txnbuild.NewThreshold(1), 32 | MediumThreshold: txnbuild.NewThreshold(1), 33 | HighThreshold: txnbuild.NewThreshold(1), 34 | Signer: &txnbuild.Signer{Address: p.Creator.Address(), Weight: 1}, 35 | }, 36 | } 37 | if !p.Asset.IsNative() { 38 | ops = append(ops, &txnbuild.ChangeTrust{ 39 | Line: p.Asset.MustToChangeTrustAsset(), 40 | Limit: amount.StringFromInt64(math.MaxInt64), 41 | SourceAccount: p.ChannelAccount.Address(), 42 | }) 43 | } 44 | ops = append(ops, &txnbuild.EndSponsoringFutureReserves{ 45 | SourceAccount: p.ChannelAccount.Address(), 46 | }) 47 | 48 | tx, err := txnbuild.NewTransaction( 49 | txnbuild.TransactionParams{ 50 | SourceAccount: &txnbuild.SimpleAccount{ 51 | AccountID: p.Creator.Address(), 52 | Sequence: p.SequenceNumber, 53 | }, 54 | BaseFee: 0, 55 | Preconditions: txnbuild.Preconditions{ 56 | TimeBounds: txnbuild.NewTimeout(300), 57 | }, 58 | Operations: ops, 59 | }, 60 | ) 61 | if err != nil { 62 | return nil, err 63 | } 64 | return tx, nil 65 | } 66 | -------------------------------------------------------------------------------- /sdk/txbuild/declaration.go: -------------------------------------------------------------------------------- 1 | package txbuild 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/stellar/go/keypair" 7 | "github.com/stellar/go/strkey" 8 | "github.com/stellar/go/txnbuild" 9 | ) 10 | 11 | type DeclarationParams struct { 12 | InitiatorChannelAccount *keypair.FromAddress 13 | StartSequence int64 14 | IterationNumber int64 15 | IterationNumberExecuted int64 16 | CloseTxHash [32]byte 17 | ConfirmingSigner *keypair.FromAddress 18 | } 19 | 20 | func Declaration(p DeclarationParams) (*txnbuild.Transaction, error) { 21 | if p.IterationNumber < 0 || p.StartSequence <= 0 { 22 | return nil, fmt.Errorf("invalid iteration number or start sequence: cannot be negative") 23 | } 24 | 25 | // Declaration is the first transaction in an iteration's transaction set. 26 | seq := startSequenceOfIteration(p.StartSequence, p.IterationNumber) + 0 27 | if seq < 0 { 28 | return nil, fmt.Errorf("invalid sequence number: cannot be negative") 29 | } 30 | 31 | minSequenceNumber := startSequenceOfIteration(p.StartSequence, p.IterationNumberExecuted) 32 | 33 | // Build the extra signature required for signing the declaration 34 | // transaction that will be required in addition to the signers for the 35 | // account signers. The extra signer will be a signature by the confirming 36 | // signer for the close transaction so that the confirming signer must 37 | // reveal that signature publicly when submitting the declaration 38 | // transaction. This prevents the confirming signer from withholding 39 | // signatures for the closing transactions. 40 | extraSigner, err := strkey.NewSignedPayload(p.ConfirmingSigner.Address(), p.CloseTxHash[:]) 41 | if err != nil { 42 | return nil, err 43 | } 44 | extraSignerStr, err := extraSigner.Encode() 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | tp := txnbuild.TransactionParams{ 50 | SourceAccount: &txnbuild.SimpleAccount{ 51 | AccountID: p.InitiatorChannelAccount.Address(), 52 | Sequence: seq, 53 | }, 54 | BaseFee: 0, 55 | Preconditions: txnbuild.Preconditions{ 56 | TimeBounds: txnbuild.NewInfiniteTimeout(), 57 | MinSequenceNumber: &minSequenceNumber, 58 | ExtraSigners: []string{ 59 | extraSignerStr, 60 | }, 61 | }, 62 | Operations: []txnbuild.Operation{ 63 | &txnbuild.BumpSequence{ 64 | BumpTo: 0, 65 | }, 66 | }, 67 | } 68 | tx, err := txnbuild.NewTransaction(tp) 69 | if err != nil { 70 | return nil, err 71 | } 72 | return tx, nil 73 | } 74 | -------------------------------------------------------------------------------- /sdk/txbuild/declaration_test.go: -------------------------------------------------------------------------------- 1 | package txbuild 2 | 3 | import ( 4 | "encoding/base64" 5 | "math" 6 | "testing" 7 | 8 | "github.com/stellar/go/keypair" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestDeclaration_size(t *testing.T) { 14 | initiatorSigner := keypair.MustRandom() 15 | responderSigner := keypair.MustRandom() 16 | initiatorChannelAccount := keypair.MustRandom() 17 | 18 | closeTxHash := [32]byte{} 19 | tx, err := Declaration(DeclarationParams{ 20 | InitiatorChannelAccount: initiatorChannelAccount.FromAddress(), 21 | StartSequence: 101, 22 | IterationNumber: 1, 23 | CloseTxHash: closeTxHash, 24 | ConfirmingSigner: initiatorSigner.FromAddress(), 25 | }) 26 | require.NoError(t, err) 27 | 28 | // Test the size without signers. 29 | { 30 | txb, err := tx.MarshalBinary() 31 | require.NoError(t, err) 32 | t.Log("unsigned:", base64.StdEncoding.EncodeToString(txb)) 33 | assert.Len(t, txb, 212) 34 | } 35 | 36 | // Test the size with signers. 37 | { 38 | tx, err := tx.Sign("test", initiatorSigner, responderSigner) 39 | require.NoError(t, err) 40 | signedPayloadSig, err := responderSigner.SignPayloadDecorated(closeTxHash[:]) 41 | require.NoError(t, err) 42 | tx, err = tx.AddSignatureDecorated(signedPayloadSig) 43 | require.NoError(t, err) 44 | txb, err := tx.MarshalBinary() 45 | require.NoError(t, err) 46 | t.Log("signed:", base64.StdEncoding.EncodeToString(txb)) 47 | assert.Len(t, txb, 428) 48 | } 49 | } 50 | 51 | func TestDeclaration_iterationNumber_checkNonNegative(t *testing.T) { 52 | _, err := Declaration(DeclarationParams{ 53 | StartSequence: 101, 54 | IterationNumber: -1, 55 | }) 56 | assert.EqualError(t, err, "invalid iteration number or start sequence: cannot be negative") 57 | _, err = Declaration(DeclarationParams{ 58 | StartSequence: -1, 59 | IterationNumber: 5, 60 | }) 61 | assert.EqualError(t, err, "invalid iteration number or start sequence: cannot be negative") 62 | } 63 | 64 | func TestDeclaration_startSequenceOfIteration_checkNonNegative(t *testing.T) { 65 | _, err := Declaration(DeclarationParams{ 66 | IterationNumber: 1, 67 | StartSequence: math.MaxInt64, 68 | }) 69 | assert.EqualError(t, err, "invalid sequence number: cannot be negative") 70 | } 71 | -------------------------------------------------------------------------------- /sdk/txbuild/doc.go: -------------------------------------------------------------------------------- 1 | // Package txbuild houses the functions for creating transactions 2 | // to create, interact with, and finally close a Starlight channel. 3 | package txbuild 4 | -------------------------------------------------------------------------------- /sdk/txbuild/open.go: -------------------------------------------------------------------------------- 1 | package txbuild 2 | 3 | import ( 4 | "math" 5 | "time" 6 | 7 | "github.com/stellar/go/amount" 8 | "github.com/stellar/go/keypair" 9 | "github.com/stellar/go/strkey" 10 | "github.com/stellar/go/txnbuild" 11 | ) 12 | 13 | type OpenParams struct { 14 | InitiatorSigner *keypair.FromAddress 15 | ResponderSigner *keypair.FromAddress 16 | InitiatorChannelAccount *keypair.FromAddress 17 | ResponderChannelAccount *keypair.FromAddress 18 | StartSequence int64 19 | Asset txnbuild.Asset 20 | ExpiresAt time.Time 21 | DeclarationTxHash [32]byte 22 | CloseTxHash [32]byte 23 | ConfirmingSigner *keypair.FromAddress 24 | } 25 | 26 | func Open(p OpenParams) (*txnbuild.Transaction, error) { 27 | // Build the list of extra signatures required for signing the open 28 | // transaction that will be required in addition to the signers for the 29 | // account signers. The extra signers will be signatures by the confirming 30 | // signer for the declaration and close transaction so that the confirming 31 | // signer must reveal those signatures publicly when submitting the 32 | // open transaction. This prevents the confirming signer from 33 | // withholding signatures for the declaration and closing transactions. 34 | extraSignerStrs := [2]string{} 35 | { 36 | extraSigner, err := strkey.NewSignedPayload(p.ConfirmingSigner.Address(), p.DeclarationTxHash[:]) 37 | if err != nil { 38 | return nil, err 39 | } 40 | extraSignerStrs[0], err = extraSigner.Encode() 41 | if err != nil { 42 | return nil, err 43 | } 44 | } 45 | { 46 | extraSigner, err := strkey.NewSignedPayload(p.ConfirmingSigner.Address(), p.CloseTxHash[:]) 47 | if err != nil { 48 | return nil, err 49 | } 50 | extraSignerStrs[1], err = extraSigner.Encode() 51 | if err != nil { 52 | return nil, err 53 | } 54 | } 55 | 56 | tp := txnbuild.TransactionParams{ 57 | SourceAccount: &txnbuild.SimpleAccount{ 58 | AccountID: p.InitiatorChannelAccount.Address(), 59 | Sequence: p.StartSequence, 60 | }, 61 | BaseFee: 0, 62 | Preconditions: txnbuild.Preconditions{ 63 | TimeBounds: txnbuild.NewTimebounds(0, p.ExpiresAt.UTC().Unix()), 64 | ExtraSigners: extraSignerStrs[:], 65 | }, 66 | } 67 | 68 | // I sponsoring ledger entries on EI 69 | tp.Operations = append(tp.Operations, &txnbuild.BeginSponsoringFutureReserves{SourceAccount: p.InitiatorSigner.Address(), SponsoredID: p.InitiatorChannelAccount.Address()}) 70 | tp.Operations = append(tp.Operations, &txnbuild.SetOptions{ 71 | SourceAccount: p.InitiatorChannelAccount.Address(), 72 | MasterWeight: txnbuild.NewThreshold(0), 73 | LowThreshold: txnbuild.NewThreshold(2), 74 | MediumThreshold: txnbuild.NewThreshold(2), 75 | HighThreshold: txnbuild.NewThreshold(2), 76 | Signer: &txnbuild.Signer{Address: p.InitiatorSigner.Address(), Weight: 1}, 77 | }) 78 | if !p.Asset.IsNative() { 79 | tp.Operations = append(tp.Operations, &txnbuild.ChangeTrust{ 80 | Line: p.Asset.MustToChangeTrustAsset(), 81 | Limit: amount.StringFromInt64(math.MaxInt64), 82 | SourceAccount: p.InitiatorChannelAccount.Address(), 83 | }) 84 | } 85 | tp.Operations = append(tp.Operations, &txnbuild.EndSponsoringFutureReserves{SourceAccount: p.InitiatorChannelAccount.Address()}) 86 | 87 | // I sponsoring ledger entries on ER 88 | tp.Operations = append(tp.Operations, &txnbuild.BeginSponsoringFutureReserves{SourceAccount: p.InitiatorSigner.Address(), SponsoredID: p.ResponderChannelAccount.Address()}) 89 | tp.Operations = append(tp.Operations, &txnbuild.SetOptions{ 90 | SourceAccount: p.ResponderChannelAccount.Address(), 91 | Signer: &txnbuild.Signer{Address: p.InitiatorSigner.Address(), Weight: 1}, 92 | }) 93 | tp.Operations = append(tp.Operations, &txnbuild.EndSponsoringFutureReserves{SourceAccount: p.ResponderChannelAccount.Address()}) 94 | 95 | // R sponsoring ledger entries on ER 96 | tp.Operations = append(tp.Operations, &txnbuild.BeginSponsoringFutureReserves{SourceAccount: p.ResponderSigner.Address(), SponsoredID: p.ResponderChannelAccount.Address()}) 97 | tp.Operations = append(tp.Operations, &txnbuild.SetOptions{ 98 | SourceAccount: p.ResponderChannelAccount.Address(), 99 | MasterWeight: txnbuild.NewThreshold(0), 100 | LowThreshold: txnbuild.NewThreshold(2), 101 | MediumThreshold: txnbuild.NewThreshold(2), 102 | HighThreshold: txnbuild.NewThreshold(2), 103 | Signer: &txnbuild.Signer{Address: p.ResponderSigner.Address(), Weight: 1}, 104 | }) 105 | if !p.Asset.IsNative() { 106 | tp.Operations = append(tp.Operations, &txnbuild.ChangeTrust{ 107 | Line: p.Asset.MustToChangeTrustAsset(), 108 | Limit: amount.StringFromInt64(math.MaxInt64), 109 | SourceAccount: p.ResponderChannelAccount.Address(), 110 | }) 111 | } 112 | tp.Operations = append(tp.Operations, &txnbuild.EndSponsoringFutureReserves{SourceAccount: p.ResponderChannelAccount.Address()}) 113 | 114 | // R sponsoring ledger entries on EI 115 | tp.Operations = append(tp.Operations, &txnbuild.BeginSponsoringFutureReserves{SourceAccount: p.ResponderSigner.Address(), SponsoredID: p.InitiatorChannelAccount.Address()}) 116 | tp.Operations = append(tp.Operations, &txnbuild.SetOptions{ 117 | SourceAccount: p.InitiatorChannelAccount.Address(), 118 | Signer: &txnbuild.Signer{Address: p.ResponderSigner.Address(), Weight: 1}, 119 | }) 120 | tp.Operations = append(tp.Operations, &txnbuild.EndSponsoringFutureReserves{SourceAccount: p.InitiatorChannelAccount.Address()}) 121 | 122 | tx, err := txnbuild.NewTransaction(tp) 123 | if err != nil { 124 | return nil, err 125 | } 126 | return tx, nil 127 | } 128 | -------------------------------------------------------------------------------- /sdk/txbuild/sequence.go: -------------------------------------------------------------------------------- 1 | package txbuild 2 | 3 | import "fmt" 4 | 5 | const m = 2 6 | 7 | func startSequenceOfIteration(startSequence int64, iterationNumber int64) int64 { 8 | return startSequence + iterationNumber*m 9 | } 10 | 11 | type TransactionType string 12 | 13 | const ( 14 | TransactionTypeUnrecognized TransactionType = "unrecognized" 15 | TransactionTypeOpen TransactionType = "open" 16 | TransactionTypeDeclaration TransactionType = "declaration" 17 | TransactionTypeClose TransactionType = "close" 18 | ) 19 | 20 | func SequenceNumberToTransactionType(startingSeqNum, seqNum int64) TransactionType { 21 | seqRelative := seqNum - startingSeqNum 22 | if seqRelative == 0 { 23 | return TransactionTypeOpen 24 | } else if seqRelative > 0 && seqRelative < m { 25 | return TransactionTypeUnrecognized 26 | } else if seqRelative%m == 0 { 27 | return TransactionTypeDeclaration 28 | } else if seqRelative%m == 1 { 29 | return TransactionTypeClose 30 | } 31 | panic(fmt.Errorf("unhandled sequence number: startingSeqNum=%d seqNum=%d", startingSeqNum, seqNum)) 32 | } 33 | -------------------------------------------------------------------------------- /sdk/txbuild/sequence_test.go: -------------------------------------------------------------------------------- 1 | package txbuild 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSequenceNumberToTransactionType(t *testing.T) { 10 | assert.Equal(t, TransactionTypeOpen, SequenceNumberToTransactionType(100, 100)) 11 | assert.Equal(t, TransactionTypeOpen, SequenceNumberToTransactionType(101, 101)) 12 | 13 | assert.Equal(t, TransactionTypeUnrecognized, SequenceNumberToTransactionType(100, 101)) 14 | assert.Equal(t, TransactionTypeUnrecognized, SequenceNumberToTransactionType(101, 102)) 15 | 16 | assert.Equal(t, TransactionTypeDeclaration, SequenceNumberToTransactionType(101, 103)) 17 | assert.Equal(t, TransactionTypeClose, SequenceNumberToTransactionType(101, 104)) 18 | 19 | assert.Equal(t, TransactionTypeDeclaration, SequenceNumberToTransactionType(100, 102)) 20 | assert.Equal(t, TransactionTypeClose, SequenceNumberToTransactionType(100, 103)) 21 | } 22 | -------------------------------------------------------------------------------- /sdk/txbuild/txbuildtest/txbuildtest.go: -------------------------------------------------------------------------------- 1 | package txbuildtest 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/stellar/go/txnbuild" 9 | "github.com/stellar/go/xdr" 10 | ) 11 | 12 | // BuildResult returns a result XDR base64 encoded that is successful or not 13 | // based on the input parameter. 14 | func BuildResultXDR(success bool) (string, error) { 15 | var code xdr.TransactionResultCode 16 | if success { 17 | code = xdr.TransactionResultCodeTxSuccess 18 | } else { 19 | code = xdr.TransactionResultCodeTxFailed 20 | } 21 | tr := xdr.TransactionResult{ 22 | FeeCharged: 123, 23 | Result: xdr.TransactionResultResult{ 24 | Code: code, 25 | Results: &[]xdr.OperationResult{}, 26 | }, 27 | } 28 | trXDR, err := xdr.MarshalBase64(tr) 29 | if err != nil { 30 | return "", fmt.Errorf("encoding transaction result to base64 xdr: %w", err) 31 | } 32 | return trXDR, nil 33 | } 34 | 35 | // BuildResultMetaXDR returns a result meta XDR base64 encoded that contains 36 | // the input ledger entry changes. Only creates one operation meta for 37 | // simiplicity. 38 | func BuildResultMetaXDR(ledgerEntryResults []xdr.LedgerEntryData) (string, error) { 39 | tm := xdr.TransactionMeta{ 40 | V: 2, 41 | V2: &xdr.TransactionMetaV2{ 42 | Operations: []xdr.OperationMeta{ 43 | {}, 44 | }, 45 | }, 46 | } 47 | 48 | rand := rand.New(rand.NewSource(time.Now().UnixNano())) 49 | for _, result := range ledgerEntryResults { 50 | change := xdr.LedgerEntryChange{} 51 | // When operations like ChangeTrustOp execute they potentially create or 52 | // update existing ledger entries. The operation encoded in the 53 | // transaction doesn't actually know which will occur because it depends 54 | // on the state of the network at the time the transaction is executed. 55 | // Randomly simulating whether a create or update change occurs 56 | // simulates that uncertainty while also ensuring we broadly test that 57 | // different behavior across all of our tests. 58 | if rand.Int()%2 == 0 { 59 | change.Type = xdr.LedgerEntryChangeTypeLedgerEntryCreated 60 | change.Created = &xdr.LedgerEntry{Data: result} 61 | } else { 62 | change.Type = xdr.LedgerEntryChangeTypeLedgerEntryUpdated 63 | change.Updated = &xdr.LedgerEntry{Data: result} 64 | } 65 | tm.V2.Operations[0].Changes = append(tm.V2.Operations[0].Changes, change) 66 | } 67 | 68 | tmXDR, err := xdr.MarshalBase64(tm) 69 | if err != nil { 70 | return "", fmt.Errorf("encoding transaction meta to base64 xdr: %w", err) 71 | } 72 | return tmXDR, nil 73 | } 74 | 75 | type OpenResultMetaParams struct { 76 | InitiatorSigner string 77 | InitiatorSignerWeight uint32 // Defaults to 1 if not set. 78 | ResponderSigner string 79 | ResponderSignerWeight uint32 // Defaults to 1 if not set. 80 | ExtraSigner string 81 | InitiatorChannelAccount string 82 | ResponderChannelAccount string 83 | Thresholds xdr.Thresholds // Defaults to 0, 2, 2, 2 if not set. 84 | StartSequence int64 85 | Asset txnbuild.Asset 86 | TrustLineFlag xdr.TrustLineFlags // Defaults to authorized flag. 87 | } 88 | 89 | func BuildOpenResultMetaXDR(params OpenResultMetaParams) (string, error) { 90 | initiatorSignerWeight := uint32(1) 91 | if params.InitiatorSignerWeight > 0 { 92 | initiatorSignerWeight = params.InitiatorSignerWeight 93 | } 94 | responderSignerWeight := uint32(1) 95 | if params.ResponderSignerWeight > 0 { 96 | responderSignerWeight = params.ResponderSignerWeight 97 | } 98 | thresholds := xdr.Thresholds{0, 2, 2, 2} 99 | if params.Thresholds != (xdr.Thresholds{}) { 100 | thresholds = params.Thresholds 101 | } 102 | led := []xdr.LedgerEntryData{ 103 | { 104 | Type: xdr.LedgerEntryTypeAccount, 105 | Account: &xdr.AccountEntry{ 106 | AccountId: xdr.MustAddress(params.InitiatorChannelAccount), 107 | SeqNum: xdr.SequenceNumber(params.StartSequence), 108 | Signers: []xdr.Signer{ 109 | { 110 | Key: xdr.MustSigner(params.InitiatorSigner), 111 | Weight: xdr.Uint32(initiatorSignerWeight), 112 | }, 113 | { 114 | Key: xdr.MustSigner(params.ResponderSigner), 115 | Weight: xdr.Uint32(responderSignerWeight), 116 | }, 117 | }, 118 | Thresholds: thresholds, 119 | }, 120 | }, 121 | { 122 | Type: xdr.LedgerEntryTypeAccount, 123 | Account: &xdr.AccountEntry{ 124 | AccountId: xdr.MustAddress(params.ResponderChannelAccount), 125 | SeqNum: xdr.SequenceNumber(1), 126 | Signers: []xdr.Signer{ 127 | { 128 | Key: xdr.MustSigner(params.InitiatorSigner), 129 | Weight: xdr.Uint32(initiatorSignerWeight), 130 | }, 131 | { 132 | Key: xdr.MustSigner(params.ResponderSigner), 133 | Weight: xdr.Uint32(responderSignerWeight), 134 | }, 135 | }, 136 | Thresholds: thresholds, 137 | }, 138 | }, 139 | } 140 | if params.ExtraSigner != "" { 141 | led[0].Account.Signers = append(led[0].Account.Signers, xdr.Signer{ 142 | Key: xdr.MustSigner(params.ExtraSigner), 143 | Weight: 1, 144 | }) 145 | led[1].Account.Signers = append(led[0].Account.Signers, xdr.Signer{ 146 | Key: xdr.MustSigner(params.ExtraSigner), 147 | Weight: 1, 148 | }) 149 | } 150 | 151 | if !params.Asset.IsNative() { 152 | trustlineFlag := xdr.TrustLineFlagsAuthorizedFlag 153 | if params.TrustLineFlag != 0 { 154 | trustlineFlag = params.TrustLineFlag 155 | } 156 | led = append(led, []xdr.LedgerEntryData{ 157 | { 158 | Type: xdr.LedgerEntryTypeTrustline, 159 | TrustLine: &xdr.TrustLineEntry{ 160 | AccountId: xdr.MustAddress(params.InitiatorChannelAccount), 161 | Balance: 0, 162 | Asset: xdr.MustNewCreditAsset(params.Asset.GetCode(), params.Asset.GetIssuer()).ToTrustLineAsset(), 163 | Flags: xdr.Uint32(trustlineFlag), 164 | }, 165 | }, 166 | { 167 | Type: xdr.LedgerEntryTypeTrustline, 168 | TrustLine: &xdr.TrustLineEntry{ 169 | AccountId: xdr.MustAddress(params.ResponderChannelAccount), 170 | Balance: 0, 171 | Asset: xdr.MustNewCreditAsset(params.Asset.GetCode(), params.Asset.GetIssuer()).ToTrustLineAsset(), 172 | Flags: xdr.Uint32(trustlineFlag), 173 | }, 174 | }, 175 | }...) 176 | } 177 | 178 | return BuildResultMetaXDR(led) 179 | } 180 | -------------------------------------------------------------------------------- /sdk/txbuild/txbuildtest/txbuildtest_test.go: -------------------------------------------------------------------------------- 1 | package txbuildtest 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stellar/go/xdr" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_txbuildtest_buildResultXDR(t *testing.T) { 12 | r, err := BuildResultXDR(true) 13 | require.NoError(t, err) 14 | 15 | var txResult xdr.TransactionResult 16 | err = xdr.SafeUnmarshalBase64(r, &txResult) 17 | require.NoError(t, err) 18 | assert.True(t, txResult.Successful()) 19 | 20 | r, err = BuildResultXDR(false) 21 | require.NoError(t, err) 22 | 23 | err = xdr.SafeUnmarshalBase64(r, &txResult) 24 | require.NoError(t, err) 25 | assert.False(t, txResult.Successful()) 26 | } 27 | 28 | func Test_txbuildtest_buildResultMetaXDR(t *testing.T) { 29 | led := []xdr.LedgerEntryData{ 30 | { 31 | Type: xdr.LedgerEntryTypeAccount, 32 | Account: &xdr.AccountEntry{ 33 | AccountId: xdr.MustAddress("GAKDNXUGEIRGESAXOPUHU4GOWLVYGQFJVHQOGFXKBXDGZ7AKMPPSDDPV"), 34 | }, 35 | }, 36 | { 37 | Type: xdr.LedgerEntryTypeTrustline, 38 | TrustLine: &xdr.TrustLineEntry{ 39 | AccountId: xdr.MustAddress("GAKDNXUGEIRGESAXOPUHU4GOWLVYGQFJVHQOGFXKBXDGZ7AKMPPSDDPV"), 40 | Balance: xdr.Int64(100), 41 | }, 42 | }, 43 | } 44 | 45 | m, err := BuildResultMetaXDR(led) 46 | require.NoError(t, err) 47 | 48 | // Validate the ledger entry changes are correct. 49 | var txMeta xdr.TransactionMeta 50 | err = xdr.SafeUnmarshalBase64(m, &txMeta) 51 | require.NoError(t, err) 52 | 53 | txMetaV2, ok := txMeta.GetV2() 54 | require.True(t, ok) 55 | 56 | for _, o := range txMetaV2.Operations { 57 | for i, change := range o.Changes { 58 | created, createdOK := change.GetCreated() 59 | updated, updatedOK := change.GetUpdated() 60 | if createdOK { 61 | assert.Equal(t, led[i], created.Data) 62 | } else if updatedOK { 63 | assert.Equal(t, led[i], updated.Data) 64 | } else { 65 | assert.Fail(t, "change didn't contain a created or updated") 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /specifications/diagrams/README.md: -------------------------------------------------------------------------------- 1 | # Payment Channel Diagrams 2 | 3 | ## How to view 4 | These diagrams were created using the [mermaid diagram live editor](https://mermaid-js.github.io/mermaid-live-editor) which uses this [syntax](https://mermaid-js.github.io/mermaid/#/sequenceDiagram). To view the diagrams below copy the text into the `code` section of the live editor. 5 | 6 | ### Sequence Diagrams 7 | [setup-payment-close.txt](./sequence-diagrams/setup-payment-close.txt) 8 | - walks through the steps of the basic case for setting up a channel, updating it, and closing it either cooperatively or non-cooperatively 9 | 10 | [withdraw.txt](./sequence-diagrams/withdraw.txt) 11 | - a withdrawal by I after setup and a single payment 12 | 13 | ### State Diagrams 14 | [channel-state.txt](./state-diagrams/channel-state.txt) 15 | - state diagram describing the possible channel states and actions that trigger movement to next states 16 | -------------------------------------------------------------------------------- /specifications/diagrams/sequence-diagrams/setup-payment-close.txt: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant I as Initiator 3 | participant R as Responder 4 | 5 | Note over I,R: 1. Setup 6 | Note right of R: i = 0, e = 0 7 | I-->>I: create and deposit funds to MI 8 | R-->>R: create and deposits funds to MR 9 | I->>R: Create open transaction F 10 | I->>I: increment i 11 | R->>R: increment i 12 | Note right of R: i = 1 13 | I->>R: Create and sign C_1 14 | R-->>I: sign C_1, send back 15 | I->>R: Create and sign D_1 16 | R-->>I: sign D_1, send back 17 | I->>R: Sign F 18 | R->>R: Sign F, submit 19 | 20 | Note over I,R: 2. Payment 21 | I->>I: increment i 22 | R->>R: increment i 23 | Note right of R: i=2 24 | I->>R: Create and sign C_2 25 | R-->>I: Sign C_2, send back 26 | I->>R: Create and sign D_2 27 | R-->>I: Sign D-2, send back 28 | 29 | Note over I,R: 3. Closing 30 | Note right of R: e = 2 31 | alt Coordinated Close 32 | I->>I: submit D_2 33 | I->>R: modify C_2 to be able to submit immediately, sign 34 | R->>R: re-sign C_2, submit 35 | else Uncoordinated Close, non-responsive actor 36 | I->>I: submit D_2 37 | I->>R: modify C_2 to be able to submit immediately, sign 38 | Note over R: R is non-responsive 39 | I->>I: wait observation period O 40 | I->>I: submit C_2 41 | else Uncoordinated Close, malicious actor 42 | I->>I: submit D_1 43 | Note over R: R notices a close from an expired payment has been initiated 44 | R->>R: submit D_2, wait observation period O to pass 45 | R->>R: submit C_2 46 | end -------------------------------------------------------------------------------- /specifications/diagrams/sequence-diagrams/withdraw.txt: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant I as Initiator 3 | participant R as Responder 4 | 5 | Note over I,R: 1. Setup and a single payment 6 | Note right of R: i = 2, e = 0 7 | Note left of I: MI.seqNum = 1000 8 | 9 | Note over I,R: 2. Withdrawal by I 10 | I->>I: increment i 11 | R->>R: increment i 12 | Note right of R: i = 3 13 | I->>R: create W_3, which has a payment op from MI to I, and bumps MI.seqNum to s_3 14 | I->>I: set e' = e, set e = i, then increment i 15 | R->>R: set e' = e, set e = i, then increment i 16 | Note right of R: e' = 0, e = 3, i = 4 17 | I->>R: create and sign C_4, which has the last agreed upon payouts minus I's payout from W_3 18 | R-->>I: sign C_4, send back 19 | I->>R: create and sign D_4 20 | R-->>I: sign D_4, send back 21 | R->>I: sign W_4 22 | I->>I: submit W_4 23 | 24 | alt W_4 successful 25 | Note right of R: withdrawal complete 26 | Note left of I: MI.seqNum = 1007 27 | else W_4 unsuccessful 28 | I->>I: set e = e' 29 | R->>R: set e = e' 30 | Note right of R: e = 0 31 | Note right of R: note: D_4.minSeq = s_3, with an unsuccessful W_3,
D_4 is impossible to submit successfully 32 | end 33 | -------------------------------------------------------------------------------- /specifications/diagrams/state-diagrams/channel-proposal.txt: -------------------------------------------------------------------------------- 1 | stateDiagram-v2 2 | [*] --> Idle 3 | note left of [*]: Initiator proposing a channel
Step1 agree on C1/D1
Step2 agree on F 4 | 5 | Idle --> Connecting: Connect() 6 | Connecting --> Listening: handshake successful 7 | Connecting --> Idle: handshake failed 8 | 9 | %% Proposing: 10 | Listening --> Proposing: Propose()
1. Create D_1, C_1
Sign C_1
Send both to R
2. Create F, sign F, send 11 | Proposing --> ResponseReceived 12 | note right of Proposing: waiting/listening while in proposing state 13 | Proposing --> NoResponseReceived: timeout expires 14 | 15 | NoResponseReceived --> Listening 16 | ResponseReceived --> Listening: Response invalid, or rejected 17 | ResponseReceived --> ProposalRejected: rejected, timeout error,
invalid response 18 | ProposalRejected --> Listening 19 | ResponseReceived --> ProposalAccepted: valid Response
1. receieved signed C_1/D_1
2. received signed F 20 | ProposalAccepted --> Listening: 1. sign D_1 and send both back
2. submit F 21 | -------------------------------------------------------------------------------- /specifications/diagrams/state-diagrams/channel-state.txt: -------------------------------------------------------------------------------- 1 | stateDiagram-v2 2 | [*] --> Waiting: NewChannel() 3 | note left of [*]: Initiator opening a channel 4 | 5 | Waiting --> Active: channel.Open() 6 | 7 | Active --> Closing: channel.CloseStart() 8 | 9 | Closing --> Closed: Responder calls
channel.CloseCoordinated() 10 | Closing --> Closed: channel.CloseUnCoordinated() 11 | 12 | Closed --> [*] 13 | -------------------------------------------------------------------------------- /specifications/diagrams/state-diagrams/payment-proposal-threads.txt: -------------------------------------------------------------------------------- 1 | stateDiagram-v2 2 | [*] --> Idle 3 | note left of [*]: Responder responding to payment 4 | note left of Idle: foreground task 5 | Idle --> Connecting: Connect() 6 | Connecting --> Ready: handshake successful 7 | Connecting --> Idle: handshake failed 8 | 9 | [*] --> Listening 10 | note left of Listening: background task 11 | Listening --> NewThread: received message 12 | state NewThread{ 13 | [*] --> ProcessingReceivedMessage 14 | ProcessingReceivedMessage --> StorePaymentProposal 15 | StorePaymentProposal --> [*] 16 | } 17 | 18 | Ready --> ConfirmingPayment: FindPaymentProposal() 19 | ConfirmingPayment --> Ready: ConfirmPayment() 20 | ConfirmingPayment --> Ready: DeclinePayment() 21 | --------------------------------------------------------------------------------