mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-14 16:15:51 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2084d61efa | ||
|
|
57b9da62bd | ||
|
|
79c583e1f7 | ||
|
|
5743dc4537 | ||
|
|
d86b1f8b7d | ||
|
|
d57f88fc18 | ||
|
|
24cf8ab8c7 | ||
|
|
e56a85cb3b | ||
|
|
d1fdea9bc8 |
@@ -407,6 +407,17 @@ target_sources (rippled PRIVATE
|
|||||||
src/ripple/app/rdb/impl/RelationalDBInterface_nodes.cpp
|
src/ripple/app/rdb/impl/RelationalDBInterface_nodes.cpp
|
||||||
src/ripple/app/rdb/impl/RelationalDBInterface_postgres.cpp
|
src/ripple/app/rdb/impl/RelationalDBInterface_postgres.cpp
|
||||||
src/ripple/app/rdb/impl/RelationalDBInterface_shards.cpp
|
src/ripple/app/rdb/impl/RelationalDBInterface_shards.cpp
|
||||||
|
src/ripple/app/sidechain/Federator.cpp
|
||||||
|
src/ripple/app/sidechain/FederatorEvents.cpp
|
||||||
|
src/ripple/app/sidechain/impl/ChainListener.cpp
|
||||||
|
src/ripple/app/sidechain/impl/DoorKeeper.cpp
|
||||||
|
src/ripple/app/sidechain/impl/InitialSync.cpp
|
||||||
|
src/ripple/app/sidechain/impl/MainchainListener.cpp
|
||||||
|
src/ripple/app/sidechain/impl/SidechainListener.cpp
|
||||||
|
src/ripple/app/sidechain/impl/SignatureCollector.cpp
|
||||||
|
src/ripple/app/sidechain/impl/SignerList.cpp
|
||||||
|
src/ripple/app/sidechain/impl/TicketHolder.cpp
|
||||||
|
src/ripple/app/sidechain/impl/WebsocketClient.cpp
|
||||||
src/ripple/app/tx/impl/ApplyContext.cpp
|
src/ripple/app/tx/impl/ApplyContext.cpp
|
||||||
src/ripple/app/tx/impl/BookTip.cpp
|
src/ripple/app/tx/impl/BookTip.cpp
|
||||||
src/ripple/app/tx/impl/CancelCheck.cpp
|
src/ripple/app/tx/impl/CancelCheck.cpp
|
||||||
@@ -576,6 +587,7 @@ target_sources (rippled PRIVATE
|
|||||||
src/ripple/rpc/handlers/DepositAuthorized.cpp
|
src/ripple/rpc/handlers/DepositAuthorized.cpp
|
||||||
src/ripple/rpc/handlers/DownloadShard.cpp
|
src/ripple/rpc/handlers/DownloadShard.cpp
|
||||||
src/ripple/rpc/handlers/Feature1.cpp
|
src/ripple/rpc/handlers/Feature1.cpp
|
||||||
|
src/ripple/rpc/handlers/FederatorInfo.cpp
|
||||||
src/ripple/rpc/handlers/Fee1.cpp
|
src/ripple/rpc/handlers/Fee1.cpp
|
||||||
src/ripple/rpc/handlers/FetchInfo.cpp
|
src/ripple/rpc/handlers/FetchInfo.cpp
|
||||||
src/ripple/rpc/handlers/GatewayBalances.cpp
|
src/ripple/rpc/handlers/GatewayBalances.cpp
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Loop: ripple.app ripple.nodestore
|
|||||||
ripple.app > ripple.nodestore
|
ripple.app > ripple.nodestore
|
||||||
|
|
||||||
Loop: ripple.app ripple.overlay
|
Loop: ripple.app ripple.overlay
|
||||||
ripple.overlay ~= ripple.app
|
ripple.overlay == ripple.app
|
||||||
|
|
||||||
Loop: ripple.app ripple.peerfinder
|
Loop: ripple.app ripple.peerfinder
|
||||||
ripple.peerfinder ~= ripple.app
|
ripple.peerfinder ~= ripple.app
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ ripple.app > ripple.crypto
|
|||||||
ripple.app > ripple.json
|
ripple.app > ripple.json
|
||||||
ripple.app > ripple.protocol
|
ripple.app > ripple.protocol
|
||||||
ripple.app > ripple.resource
|
ripple.app > ripple.resource
|
||||||
|
ripple.app > ripple.server
|
||||||
ripple.app > test.unit_test
|
ripple.app > test.unit_test
|
||||||
ripple.basics > ripple.beast
|
ripple.basics > ripple.beast
|
||||||
ripple.conditions > ripple.basics
|
ripple.conditions > ripple.basics
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ if(Git_FOUND)
|
|||||||
endif()
|
endif()
|
||||||
endif() #git
|
endif() #git
|
||||||
|
|
||||||
|
if (thread_safety_analysis)
|
||||||
|
add_compile_options(-Wthread-safety -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS -DRIPPLE_ENABLE_THREAD_SAFETY_ANNOTATIONS)
|
||||||
|
add_compile_options("-stdlib=libc++")
|
||||||
|
add_link_options("-stdlib=libc++")
|
||||||
|
endif()
|
||||||
|
|
||||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/Builds/CMake")
|
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/Builds/CMake")
|
||||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/Builds/CMake/deps")
|
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/Builds/CMake/deps")
|
||||||
|
|
||||||
|
|||||||
94
README.md
94
README.md
@@ -1,3 +1,97 @@
|
|||||||
|
# XRP Ledger Side chain Branch
|
||||||
|
|
||||||
|
## Warning
|
||||||
|
|
||||||
|
This is not the main branch of the XRP Ledger. This branch supports side chains
|
||||||
|
on the XRP ledger and integrates an implementation of side chain federators.
|
||||||
|
This is a developers prelease and it should not be used for production or to
|
||||||
|
transfer real value. Consider this "alpha" quality software. There will be bugs.
|
||||||
|
See "Status" for a fuller description.
|
||||||
|
|
||||||
|
The latest production code for the XRP ledger is on the "master" branch.
|
||||||
|
|
||||||
|
Until this branch is merged with the mainline branch, it will periodically be
|
||||||
|
rebased on that branch and force pushed to github.
|
||||||
|
|
||||||
|
## What are side chains?
|
||||||
|
|
||||||
|
Side chains are independent ledgers. They can have their own transaction types,
|
||||||
|
their own UNL list (or even their own consensus algorithm), and their own set of
|
||||||
|
other unique features (like a smart contracts layer). What's special about them
|
||||||
|
is there is a way to transfer assets from the XRP ledger to the side chain, and
|
||||||
|
a way to return those assets back from the side chain to the XRP ledger. Both
|
||||||
|
XRP and issued assets may be exchanged.
|
||||||
|
|
||||||
|
The motivation for creating a side chain is to implement an idea that may not be
|
||||||
|
a great fit for the main XRP ledger, or may take a long time before such a
|
||||||
|
feature is adopted by the main XRP ledger. The advantage of a side chain over a
|
||||||
|
brand new ledger is it allows the side chain to immediate use tokens with real
|
||||||
|
monetary value.
|
||||||
|
|
||||||
|
This implementation is meant to support side chains that are similar to the XRP
|
||||||
|
ledger and use the XRP ledger as the main chain. The idea is to develop a new
|
||||||
|
side chain, first this code will be forked and the new features specific to the
|
||||||
|
new chain will be implemented.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
All the functionality needed to build side chains should be complete. However,
|
||||||
|
it has not been well tested or polished.
|
||||||
|
|
||||||
|
In particular, all of the following are built:
|
||||||
|
|
||||||
|
* Cross chain transactions for both XRP and Issued Assets
|
||||||
|
* Refunds if transactions fail
|
||||||
|
* Allowing federators to rejoin a network
|
||||||
|
* Detecting and handling when federators fall too far behind in processing
|
||||||
|
transactions
|
||||||
|
* A python library to easily create configuration files for testing side chains
|
||||||
|
and spin up side chains on a local machine
|
||||||
|
* Python scripts to test side chains
|
||||||
|
* An interactive shell to explore side chain functionality
|
||||||
|
|
||||||
|
The biggest missing pieces are:
|
||||||
|
|
||||||
|
* Testing: While the functionality is there, it has just begun to be tested.
|
||||||
|
There will be bugs. Even horrible and embarrassing bugs. Of course, this will
|
||||||
|
improve as testing progresses.
|
||||||
|
|
||||||
|
* Tooling: There is a python library and an interactive shell that was built to
|
||||||
|
help development. However, these tools are geared to run a test network on a
|
||||||
|
local machine. They are not geared to new users or to production systems.
|
||||||
|
Better tooling is coming.
|
||||||
|
|
||||||
|
* Documentation: There is documentation that describes the technical details of
|
||||||
|
how side chains works, how to run the python scripts to set up side chains on
|
||||||
|
the local machine, and the changes to the configuration files. However, like
|
||||||
|
the tooling, this is not geared to new users or production systems. Better
|
||||||
|
documentation is coming. In particular, good documentation for how to set up a
|
||||||
|
production side chain - or even a test net that doesn't run on a local
|
||||||
|
machine - needs to be written.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
See the instructions [here](docs/sidechain/GettingStarted.md) for how to
|
||||||
|
run an interactive shell that will spin up a set of federators on your local
|
||||||
|
machine and allow you to transfer assets between the main chain and a side
|
||||||
|
chain.
|
||||||
|
|
||||||
|
After setting things up and completing a cross chain transaction with the
|
||||||
|
"getting started" script above, it may to useful to browse some other
|
||||||
|
documentation:
|
||||||
|
|
||||||
|
* [This](bin/sidechain/python/README.md) document describes the scripts and
|
||||||
|
python modules used to test and explore side chains on your local machine.
|
||||||
|
|
||||||
|
* [This](docs/sidechain/configFile.md) document describes the new stanzas in the
|
||||||
|
config file needed for side chains.
|
||||||
|
|
||||||
|
* [This](docs/sidechain/federatorAccountSetup.md) document describes how to set
|
||||||
|
up the federator accounts if not using the python scripts.
|
||||||
|
|
||||||
|
* [This](docs/sidechain/design.md) document describes the low-level details for
|
||||||
|
how side chains work.
|
||||||
|
|
||||||
# The XRP Ledger
|
# The XRP Ledger
|
||||||
|
|
||||||
The [XRP Ledger](https://xrpl.org/) is a decentralized cryptographic ledger powered by a network of peer-to-peer nodes. The XRP Ledger uses a novel Byzantine Fault Tolerant consensus algorithm to settle and record transactions in a secure distributed database without a central operator.
|
The [XRP Ledger](https://xrpl.org/) is a decentralized cryptographic ledger powered by a network of peer-to-peer nodes. The XRP Ledger uses a novel Byzantine Fault Tolerant consensus algorithm to settle and record transactions in a secure distributed database without a central operator.
|
||||||
|
|||||||
183
bin/sidechain/python/README.md
Normal file
183
bin/sidechain/python/README.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
## Introduction
|
||||||
|
|
||||||
|
This directory contains python scripts to tests and explore side chains.
|
||||||
|
|
||||||
|
See the instructions [here](docs/sidechain/GettingStarted.md) for how to install
|
||||||
|
the necessary dependencies and run an interactive shell that will spin up a set
|
||||||
|
of federators on your local machine and allow you to transfer assets between the
|
||||||
|
main chain and a side chain.
|
||||||
|
|
||||||
|
For all these scripts, make sure the `RIPPLED_MAINCHAIN_EXE`,
|
||||||
|
`RIPPLED_SIDECHAIN_EXE`, and `RIPPLED_SIDECHAIN_CFG_DIR` environment variables
|
||||||
|
are correctly set, and the side chain configuration files exist. Also make sure the python
|
||||||
|
dependencies are installed and the virtual environment is activated.
|
||||||
|
|
||||||
|
Note: the unit tests do not use the configuration files, so the `RIPPLED_SIDECHAIN_CFG_DIR` is
|
||||||
|
not needed for that script.
|
||||||
|
|
||||||
|
## Unit tests
|
||||||
|
|
||||||
|
The "tests" directory contains a simple unit test. It take several minutes to
|
||||||
|
run, and will create the necessary configuration files, start a test main chain
|
||||||
|
in standalone mode, and a test side chain with 5 federators, and do some simple
|
||||||
|
cross chain transactions. Side chains do not yet have extensive tests. Testing
|
||||||
|
is being actively worked on.
|
||||||
|
|
||||||
|
To run the tests, change directories to the `bin/sidechain/python/tests` directory and type:
|
||||||
|
```
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
To capture logging information and to set the log level (to help with debugging), type this instead:
|
||||||
|
```
|
||||||
|
pytest --log-file=log.txt --log-file-level=info
|
||||||
|
```
|
||||||
|
|
||||||
|
The response should be something like the following:
|
||||||
|
```
|
||||||
|
============================= test session starts ==============================
|
||||||
|
platform linux -- Python 3.8.5, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
||||||
|
rootdir: /home/swd/projs/ripple/mine/bin/sidechain/python/tests
|
||||||
|
collected 1 item
|
||||||
|
|
||||||
|
simple_xchain_transfer_test.py . [100%]
|
||||||
|
|
||||||
|
======================== 1 passed in 215.20s (0:03:35) =========================
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
### riplrepl.py
|
||||||
|
|
||||||
|
This is an interactive shell for experimenting with side chains. It will spin up
|
||||||
|
a test main chain running in standalone mode, and a test side chain with five
|
||||||
|
federators - all running on the local machine. There are commands to make
|
||||||
|
payments within a chain, make cross chain payments, check balances, check server
|
||||||
|
info, and check federator info. There is a simple "help" system, but more
|
||||||
|
documentation is needed for this tool (or more likely we need to replace this
|
||||||
|
with some web front end).
|
||||||
|
|
||||||
|
Note: a "repl" is another name for an interactive shell. It stands for
|
||||||
|
"read-eval-print-loop". It is pronounced "rep-ul".
|
||||||
|
|
||||||
|
### create_config_file.py
|
||||||
|
|
||||||
|
This is a script used to create the config files needed to run a test side chain
|
||||||
|
on your local machine. To run this, make sure the rippled is built,
|
||||||
|
`RIPPLED_MAINCHAIN_EXE`, `RIPPLED_SIDECHAIN_EXE`, and
|
||||||
|
`RIPPLED_SIDECHAIN_CFG_DIR` environment variables are correctly set, and the
|
||||||
|
side chain configuration files exist. Also make sure the python dependencies are
|
||||||
|
installed and the virtual environment is activated. Running this will create
|
||||||
|
config files in the directory specified by the `RIPPLED_SIDECHAIN_CFG_DIR`
|
||||||
|
environment variable.
|
||||||
|
|
||||||
|
### log_analyzer.py
|
||||||
|
|
||||||
|
This is a script used to take structured log files and convert them to json for easier debugging.
|
||||||
|
|
||||||
|
## Python modules
|
||||||
|
|
||||||
|
### sidechain.py
|
||||||
|
|
||||||
|
A python module that can be used to write python scripts to interact with
|
||||||
|
side chains. This is used to write unit tests and the interactive shell. To write
|
||||||
|
a standalone script, look at how the tests are written in
|
||||||
|
`test/simple_xchain_transfer_test.py`. The idea is to call
|
||||||
|
`sidechain._multinode_with_callback`, which sets up the two chains, and place
|
||||||
|
your code in the callback. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
def multinode_test(params: Params):
|
||||||
|
def callback(mc_app: App, sc_app: App):
|
||||||
|
my_function(mc_app, sc_app, params)
|
||||||
|
|
||||||
|
sidechain._multinode_with_callback(params,
|
||||||
|
callback,
|
||||||
|
setup_user_accounts=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
The functions `sidechain.main_to_side_transfer` and
|
||||||
|
`sidechain.side_to_main_transfer` can be used as convenience functions to initiate
|
||||||
|
cross chain transfers. Of course, these transactions can also be initiated with
|
||||||
|
a payment to the door account with the memo data set to the destination account
|
||||||
|
on the destination chain (which is what those convenience functions do under the
|
||||||
|
hood).
|
||||||
|
|
||||||
|
Transactions execute asynchonously. Use the function
|
||||||
|
`test_utils.wait_for_balance_change` to ensure a transaction has completed.
|
||||||
|
|
||||||
|
### transaction.py
|
||||||
|
|
||||||
|
A python module for transactions. Currently there are transactions for:
|
||||||
|
|
||||||
|
* Payment
|
||||||
|
* Trust (trust set)
|
||||||
|
* SetRegularKey
|
||||||
|
* SignerLisetSet
|
||||||
|
* AccountSet
|
||||||
|
* Offer
|
||||||
|
* Ticket
|
||||||
|
* Hook (experimental - useful paying with the hook amendment from XRPL Labs).
|
||||||
|
|
||||||
|
Typically, a transaction is submitted through the call operator on an `App` object. For example, to make a payment from the account `alice` to the account `bob` for 500 XRP:
|
||||||
|
```
|
||||||
|
mc_app(Payment(account=alice, dst=bob, amt=XRP(500)))
|
||||||
|
```
|
||||||
|
(where mc_app is an App object representing the main chain).
|
||||||
|
|
||||||
|
### command.py
|
||||||
|
|
||||||
|
A python module for RPC commands. Currently there are commands for:
|
||||||
|
* PathFind
|
||||||
|
* Sign
|
||||||
|
* LedgerAccept (for standalone mode)
|
||||||
|
* Stop
|
||||||
|
* LogLevel
|
||||||
|
* WalletPropose
|
||||||
|
* ValidationCreate
|
||||||
|
* AccountInfo
|
||||||
|
* AccountLines
|
||||||
|
* AccountTx
|
||||||
|
* BookOffers
|
||||||
|
* BookSubscription
|
||||||
|
* ServerInfo
|
||||||
|
* FederatorInfo
|
||||||
|
* Subscribe
|
||||||
|
|
||||||
|
### common.py
|
||||||
|
|
||||||
|
Python module for common ledger objects, including:
|
||||||
|
* Account
|
||||||
|
* Asset
|
||||||
|
* Path
|
||||||
|
* Pathlist
|
||||||
|
|
||||||
|
### app.py
|
||||||
|
|
||||||
|
Python module for an application. An application is responsible for local
|
||||||
|
network (or single server) and an address book that maps aliases to accounts.
|
||||||
|
|
||||||
|
### config_file.py
|
||||||
|
|
||||||
|
Python module representing a config file that is read from disk.
|
||||||
|
|
||||||
|
### interactive.py
|
||||||
|
|
||||||
|
Python module with the implementation of the RiplRepl interactive shell.
|
||||||
|
|
||||||
|
### ripple_client.py
|
||||||
|
|
||||||
|
A python module representing a rippled server.
|
||||||
|
|
||||||
|
### testnet.py
|
||||||
|
|
||||||
|
A python module representing a rippled testnet running on the local machine.
|
||||||
|
|
||||||
|
## Other
|
||||||
|
### requirements.txt
|
||||||
|
|
||||||
|
These are the python dependencies needed by the scripts. Use `pip3 install -r
|
||||||
|
requirements.txt` to install them. A python virtual environment is recommended.
|
||||||
|
See the instructions [here](docs/sidechain/GettingStarted.md) for how to install
|
||||||
|
the dependencies.
|
||||||
|
|
||||||
624
bin/sidechain/python/app.py
Normal file
624
bin/sidechain/python/app.py
Normal file
@@ -0,0 +1,624 @@
|
|||||||
|
from contextlib import contextmanager
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from typing import Callable, Dict, List, Optional, Set, Union
|
||||||
|
|
||||||
|
from ripple_client import RippleClient
|
||||||
|
from common import Account, Asset, XRP
|
||||||
|
from command import AccountInfo, AccountLines, BookOffers, Command, FederatorInfo, LedgerAccept, Sign, Submit, SubscriptionCommand, WalletPropose
|
||||||
|
from config_file import ConfigFile
|
||||||
|
import testnet
|
||||||
|
from transaction import Payment, Transaction
|
||||||
|
|
||||||
|
|
||||||
|
class KeyManager:
|
||||||
|
def __init__(self):
|
||||||
|
self._aliases = {} # alias -> account
|
||||||
|
self._accounts = {} # account id -> account
|
||||||
|
|
||||||
|
def add(self, account: Account) -> bool:
|
||||||
|
if account.nickname:
|
||||||
|
self._aliases[account.nickname] = account
|
||||||
|
self._accounts[account.account_id] = account
|
||||||
|
|
||||||
|
def is_alias(self, name: str):
|
||||||
|
return name in self._aliases
|
||||||
|
|
||||||
|
def account_from_alias(self, name: str) -> Account:
|
||||||
|
assert name in self._aliases
|
||||||
|
return self._aliases[name]
|
||||||
|
|
||||||
|
def known_accounts(self) -> List[Account]:
|
||||||
|
return list(self._accounts.values())
|
||||||
|
|
||||||
|
def account_id_dict(self) -> Dict[str, Account]:
|
||||||
|
return self._accounts
|
||||||
|
|
||||||
|
def alias_or_account_id(self, id: Union[Account, str]) -> str:
|
||||||
|
'''
|
||||||
|
return the alias if it exists, otherwise return the id
|
||||||
|
'''
|
||||||
|
if isinstance(id, Account):
|
||||||
|
return id.alias_or_account_id()
|
||||||
|
|
||||||
|
if id in self._accounts:
|
||||||
|
return self._accounts[id].nickname
|
||||||
|
return id
|
||||||
|
|
||||||
|
def alias_to_account_id(self, alias: str) -> Optional[str]:
|
||||||
|
if id in self._aliases:
|
||||||
|
return self._aliases[id].account_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
def to_string(self, nickname: Optional[str] = None):
|
||||||
|
names = []
|
||||||
|
account_ids = []
|
||||||
|
if nickname:
|
||||||
|
names = [nickname]
|
||||||
|
if nickname in self._aliases:
|
||||||
|
account_ids = [self._aliases[nickname].account_id]
|
||||||
|
else:
|
||||||
|
account_id = ['NA']
|
||||||
|
else:
|
||||||
|
for (k, v) in self._aliases.items():
|
||||||
|
names.append(k)
|
||||||
|
account_ids.append(v.account_id)
|
||||||
|
# use a dataframe to get a nice table output
|
||||||
|
df = pd.DataFrame(data={'name': names, 'id': account_ids})
|
||||||
|
return f'{df.to_string(index=False)}'
|
||||||
|
|
||||||
|
|
||||||
|
class AssetAliases:
|
||||||
|
def __init__(self):
|
||||||
|
self._aliases = {} # alias -> asset
|
||||||
|
|
||||||
|
def add(self, asset: Asset, name: str):
|
||||||
|
self._aliases[name] = asset
|
||||||
|
|
||||||
|
def is_alias(self, name: str):
|
||||||
|
return name in self._aliases
|
||||||
|
|
||||||
|
def asset_from_alias(self, name: str) -> Asset:
|
||||||
|
assert name in self._aliases
|
||||||
|
return self._aliases[name]
|
||||||
|
|
||||||
|
def known_aliases(self) -> List[str]:
|
||||||
|
return list(self._aliases.keys())
|
||||||
|
|
||||||
|
def known_assets(self) -> List[Asset]:
|
||||||
|
return list(self._aliases.values())
|
||||||
|
|
||||||
|
def to_string(self, nickname: Optional[str] = None):
|
||||||
|
names = []
|
||||||
|
currencies = []
|
||||||
|
issuers = []
|
||||||
|
if nickname:
|
||||||
|
names = [nickname]
|
||||||
|
if nickname in self._aliases:
|
||||||
|
v = self._aliases[nickname]
|
||||||
|
currencies = [v.currency]
|
||||||
|
iss = v.issuer if v.issuer else ''
|
||||||
|
issuers = [v.issuer if v.issuer else '']
|
||||||
|
else:
|
||||||
|
currencies = ['NA']
|
||||||
|
issuers = ['NA']
|
||||||
|
else:
|
||||||
|
for (k, v) in self._aliases.items():
|
||||||
|
names.append(k)
|
||||||
|
currencies.append(v.currency)
|
||||||
|
issuers.append(v.issuer if v.issuer else '')
|
||||||
|
# use a dataframe to get a nice table output
|
||||||
|
df = pd.DataFrame(data={
|
||||||
|
'name': names,
|
||||||
|
'currency': currencies,
|
||||||
|
'issuer': issuers
|
||||||
|
})
|
||||||
|
return f'{df.to_string(index=False)}'
|
||||||
|
|
||||||
|
|
||||||
|
class App:
|
||||||
|
'''App to to interact with rippled servers'''
|
||||||
|
def __init__(self,
|
||||||
|
*,
|
||||||
|
standalone: bool,
|
||||||
|
network: Optional[testnet.Network] = None,
|
||||||
|
client: Optional[RippleClient] = None):
|
||||||
|
if network and client:
|
||||||
|
raise ValueError('Cannot specify both a testnet and client in App')
|
||||||
|
if not network and not client:
|
||||||
|
raise ValueError('Must specify a testnet or a client in App')
|
||||||
|
|
||||||
|
self.standalone = standalone
|
||||||
|
self.network = network
|
||||||
|
|
||||||
|
if client:
|
||||||
|
self.client = client
|
||||||
|
else:
|
||||||
|
self.client = self.network.get_client(0)
|
||||||
|
|
||||||
|
self.key_manager = KeyManager()
|
||||||
|
self.asset_aliases = AssetAliases()
|
||||||
|
root_account = Account(nickname='root',
|
||||||
|
account_id='rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
|
||||||
|
secret_key='masterpassphrase')
|
||||||
|
self.key_manager.add(root_account)
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
if self.network:
|
||||||
|
self.network.shutdown()
|
||||||
|
else:
|
||||||
|
self.client.shutdown()
|
||||||
|
|
||||||
|
def send_signed(self, txn: Transaction) -> dict:
|
||||||
|
'''Sign then send the given transaction'''
|
||||||
|
if not txn.account.secret_key:
|
||||||
|
raise ValueError('Cannot sign transaction without secret key')
|
||||||
|
r = self(Sign(txn.account.secret_key, txn.to_cmd_obj()))
|
||||||
|
raw_signed = r['tx_blob']
|
||||||
|
r = self(Submit(raw_signed))
|
||||||
|
logging.info(f'App.send_signed: {json.dumps(r, indent=1)}')
|
||||||
|
return r
|
||||||
|
|
||||||
|
def send_command(self, cmd: Command) -> dict:
|
||||||
|
'''Send the command to the rippled server'''
|
||||||
|
r = self.client.send_command(cmd)
|
||||||
|
logging.info(
|
||||||
|
f'App.send_command : {cmd.cmd_name()} : {json.dumps(r, indent=1)}')
|
||||||
|
return r
|
||||||
|
|
||||||
|
# Need async version to close ledgers from async functions
|
||||||
|
async def async_send_command(self, cmd: Command) -> dict:
|
||||||
|
'''Send the command to the rippled server'''
|
||||||
|
return await self.client.async_send_command(cmd)
|
||||||
|
|
||||||
|
def send_subscribe_command(
|
||||||
|
self,
|
||||||
|
cmd: SubscriptionCommand,
|
||||||
|
callback: Optional[Callable[[dict], None]] = None) -> dict:
|
||||||
|
'''Send the subscription command to the rippled server. If already subscribed, it will unsubscribe'''
|
||||||
|
return self.client.send_subscribe_command(cmd, callback)
|
||||||
|
|
||||||
|
def get_pids(self) -> List[int]:
|
||||||
|
if self.network:
|
||||||
|
return self.network.get_pids()
|
||||||
|
if pid := self.client.get_pid():
|
||||||
|
return [pid]
|
||||||
|
|
||||||
|
def get_running_status(self) -> List[bool]:
|
||||||
|
if self.network:
|
||||||
|
return self.network.get_running_status()
|
||||||
|
if self.client.get_pid():
|
||||||
|
return [True]
|
||||||
|
else:
|
||||||
|
return [False]
|
||||||
|
|
||||||
|
# Get a dict of the server_state, validated_ledger_seq, and complete_ledgers
|
||||||
|
def get_brief_server_info(self) -> dict:
|
||||||
|
if self.network:
|
||||||
|
return self.network.get_brief_server_info()
|
||||||
|
else:
|
||||||
|
ret = {}
|
||||||
|
for (k, v) in self.client.get_brief_server_info().items():
|
||||||
|
ret[k] = [v]
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def servers_start(self,
|
||||||
|
server_indexes: Optional[Union[Set[int],
|
||||||
|
List[int]]] = None,
|
||||||
|
*,
|
||||||
|
extra_args: Optional[List[List[str]]] = None):
|
||||||
|
if self.network:
|
||||||
|
return self.network.servers_start(server_indexes,
|
||||||
|
extra_args=extra_args)
|
||||||
|
else:
|
||||||
|
raise ValueError('Cannot start stand alone server')
|
||||||
|
|
||||||
|
def servers_stop(self,
|
||||||
|
server_indexes: Optional[Union[Set[int],
|
||||||
|
List[int]]] = None):
|
||||||
|
if self.network:
|
||||||
|
return self.network.servers_stop(server_indexes)
|
||||||
|
else:
|
||||||
|
raise ValueError('Cannot stop stand alone server')
|
||||||
|
|
||||||
|
def federator_info(self,
|
||||||
|
server_indexes: Optional[Union[Set[int],
|
||||||
|
List[int]]] = None):
|
||||||
|
# key is server index. value is federator_info result
|
||||||
|
result_dict = {}
|
||||||
|
if self.network:
|
||||||
|
if not server_indexes:
|
||||||
|
server_indexes = [
|
||||||
|
i for i in range(self.network.num_clients())
|
||||||
|
if self.network.is_running(i)
|
||||||
|
]
|
||||||
|
for i in server_indexes:
|
||||||
|
if self.network.is_running(i):
|
||||||
|
result_dict[i] = self.network.get_client(i).send_command(
|
||||||
|
FederatorInfo())
|
||||||
|
else:
|
||||||
|
if 0 in server_indexes:
|
||||||
|
result_dict[0] = self.client.send_command(FederatorInfo())
|
||||||
|
return result_dict
|
||||||
|
|
||||||
|
def __call__(self,
|
||||||
|
to_send: Union[Transaction, Command, SubscriptionCommand],
|
||||||
|
callback: Optional[Callable[[dict], None]] = None,
|
||||||
|
*,
|
||||||
|
insert_seq_and_fee=False) -> dict:
|
||||||
|
'''Call `send_signed` for transactions or `send_command` for commands'''
|
||||||
|
if isinstance(to_send, SubscriptionCommand):
|
||||||
|
return self.send_subscribe_command(to_send, callback)
|
||||||
|
assert callback is None
|
||||||
|
if isinstance(to_send, Transaction):
|
||||||
|
if insert_seq_and_fee:
|
||||||
|
self.insert_seq_and_fee(to_send)
|
||||||
|
return self.send_signed(to_send)
|
||||||
|
if isinstance(to_send, Command):
|
||||||
|
return self.send_command(to_send)
|
||||||
|
raise ValueError(
|
||||||
|
'Expected `to_send` to be either a Transaction, Command, or SubscriptionCommand'
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_configs(self) -> List[str]:
|
||||||
|
if self.network:
|
||||||
|
return self.network.get_configs()
|
||||||
|
return [self.client.config]
|
||||||
|
|
||||||
|
def create_account(self, name: str) -> Account:
|
||||||
|
''' Create an account. Use the name as the alias. '''
|
||||||
|
if name == 'root':
|
||||||
|
return
|
||||||
|
assert not self.key_manager.is_alias(name)
|
||||||
|
|
||||||
|
account = Account(nickname=name, result_dict=self(WalletPropose()))
|
||||||
|
self.key_manager.add(account)
|
||||||
|
return account
|
||||||
|
|
||||||
|
def create_accounts(self,
|
||||||
|
names: List[str],
|
||||||
|
funding_account: Union[Account, str] = 'root',
|
||||||
|
amt: Union[int, Asset] = 1000000000) -> List[Account]:
|
||||||
|
'''Fund the accounts with nicknames 'names' by using the funding account and amt'''
|
||||||
|
accounts = [self.create_account(n) for n in names]
|
||||||
|
if not isinstance(funding_account, Account):
|
||||||
|
org_funding_account = funding_account
|
||||||
|
funding_account = self.key_manager.account_from_alias(
|
||||||
|
funding_account)
|
||||||
|
if not isinstance(funding_account, Account):
|
||||||
|
raise ValueError(
|
||||||
|
f'Could not find funding account {org_funding_account}')
|
||||||
|
if not isinstance(amt, Asset):
|
||||||
|
assert isinstance(amt, int)
|
||||||
|
amt = Asset(value=amt)
|
||||||
|
for a in accounts:
|
||||||
|
p = Payment(account=funding_account, dst=a, amt=amt)
|
||||||
|
self.send_signed(p)
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
def maybe_ledger_accept(self):
|
||||||
|
if not self.standalone:
|
||||||
|
return
|
||||||
|
self(LedgerAccept())
|
||||||
|
|
||||||
|
# Need async version to close ledgers from async functions
|
||||||
|
async def async_maybe_ledger_accept(self):
|
||||||
|
if not self.standalone:
|
||||||
|
return
|
||||||
|
await self.async_send_command(LedgerAccept())
|
||||||
|
|
||||||
|
def get_balances(
|
||||||
|
self,
|
||||||
|
account: Union[Account, List[Account], None] = None,
|
||||||
|
asset: Union[Asset, List[Asset]] = Asset()
|
||||||
|
) -> pd.DataFrame:
|
||||||
|
'''Return a pandas dataframe of account balances. If account is None, treat as a wildcard (use address book)'''
|
||||||
|
if account is None:
|
||||||
|
account = self.key_manager.known_accounts()
|
||||||
|
if isinstance(account, list):
|
||||||
|
result = [self.get_balances(acc, asset) for acc in account]
|
||||||
|
return pd.concat(result, ignore_index=True)
|
||||||
|
if isinstance(asset, list):
|
||||||
|
result = [self.get_balances(account, ass) for ass in asset]
|
||||||
|
return pd.concat(result, ignore_index=True)
|
||||||
|
if asset.is_xrp():
|
||||||
|
try:
|
||||||
|
df = self.get_account_info(account)
|
||||||
|
except:
|
||||||
|
# Most likely the account does not exist on the ledger. Give a balance of zero.
|
||||||
|
df = pd.DataFrame({
|
||||||
|
'account': [account],
|
||||||
|
'balance': [0],
|
||||||
|
'flags': [0],
|
||||||
|
'owner_count': [0],
|
||||||
|
'previous_txn_id': ['NA'],
|
||||||
|
'previous_txn_lgr_seq': [-1],
|
||||||
|
'sequence': [-1]
|
||||||
|
})
|
||||||
|
df = df.assign(currency='XRP', peer='', limit='')
|
||||||
|
return df.loc[:,
|
||||||
|
['account', 'balance', 'currency', 'peer', 'limit']]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
df = self.get_trust_lines(account)
|
||||||
|
if df.empty: return df
|
||||||
|
df = df[(df['peer'] == asset.issuer.account_id)
|
||||||
|
& (df['currency'] == asset.currency)]
|
||||||
|
except:
|
||||||
|
# Most likely the account does not exist on the ledger. Return an empty data frame
|
||||||
|
df = pd.DataFrame({
|
||||||
|
'account': [],
|
||||||
|
'balance': [],
|
||||||
|
'currency': [],
|
||||||
|
'peer': [],
|
||||||
|
'limit': [],
|
||||||
|
})
|
||||||
|
return df.loc[:,
|
||||||
|
['account', 'balance', 'currency', 'peer', 'limit']]
|
||||||
|
|
||||||
|
def get_balance(self, account: Account, asset: Asset) -> Asset:
|
||||||
|
'''Get a balance from a single account in a single asset'''
|
||||||
|
try:
|
||||||
|
df = self.get_balances(account, asset)
|
||||||
|
return asset(df.iloc[0]['balance'])
|
||||||
|
except:
|
||||||
|
return asset(0)
|
||||||
|
|
||||||
|
def get_account_info(self,
|
||||||
|
account: Optional[Account] = None) -> pd.DataFrame:
|
||||||
|
'''Return a pandas dataframe of account info. If account is None, treat as a wildcard (use address book)'''
|
||||||
|
if account is None:
|
||||||
|
known_accounts = self.key_manager.known_accounts()
|
||||||
|
result = [self.get_account_info(acc) for acc in known_accounts]
|
||||||
|
return pd.concat(result, ignore_index=True)
|
||||||
|
try:
|
||||||
|
result = self.client.send_command(AccountInfo(account))
|
||||||
|
except:
|
||||||
|
# Most likely the account does not exist on the ledger. Give a balance of zero.
|
||||||
|
return pd.DataFrame({
|
||||||
|
'account': [account],
|
||||||
|
'balance': [0],
|
||||||
|
'flags': [0],
|
||||||
|
'owner_count': [0],
|
||||||
|
'previous_txn_id': ['NA'],
|
||||||
|
'previous_txn_lgr_seq': [-1],
|
||||||
|
'sequence': [-1]
|
||||||
|
})
|
||||||
|
if 'account_data' not in result:
|
||||||
|
raise ValueError('Bad result from account_info command')
|
||||||
|
info = result['account_data']
|
||||||
|
for dk in ['LedgerEntryType', 'index']:
|
||||||
|
del info[dk]
|
||||||
|
df = pd.DataFrame([info])
|
||||||
|
df.rename(columns={
|
||||||
|
'Account': 'account',
|
||||||
|
'Balance': 'balance',
|
||||||
|
'Flags': 'flags',
|
||||||
|
'OwnerCount': 'owner_count',
|
||||||
|
'PreviousTxnID': 'previous_txn_id',
|
||||||
|
'PreviousTxnLgrSeq': 'previous_txn_lgr_seq',
|
||||||
|
'Sequence': 'sequence'
|
||||||
|
},
|
||||||
|
inplace=True)
|
||||||
|
df['balance'] = df['balance'].astype(int)
|
||||||
|
return df
|
||||||
|
|
||||||
|
def get_trust_lines(self,
|
||||||
|
account: Account,
|
||||||
|
peer: Optional[Account] = None) -> pd.DataFrame:
|
||||||
|
'''Return a pandas dataframe account trust lines. If peer account is None, treat as a wildcard'''
|
||||||
|
result = self.send_command(AccountLines(account, peer=peer))
|
||||||
|
if 'lines' not in result or 'account' not in result:
|
||||||
|
raise ValueError('Bad result from account_lines command')
|
||||||
|
account = result['account']
|
||||||
|
lines = result['lines']
|
||||||
|
for d in lines:
|
||||||
|
d['peer'] = d['account']
|
||||||
|
d['account'] = account
|
||||||
|
return pd.DataFrame(lines)
|
||||||
|
|
||||||
|
def get_offers(self, taker_pays: Asset, taker_gets: Asset) -> pd.DataFrame:
|
||||||
|
'''Return a pandas dataframe of offers'''
|
||||||
|
result = self.send_command(BookOffers(taker_pays, taker_gets))
|
||||||
|
if 'offers' not in result:
|
||||||
|
raise ValueError('Bad result from book_offers command')
|
||||||
|
|
||||||
|
offers = result['offers']
|
||||||
|
delete_keys = [
|
||||||
|
'BookDirectory', 'BookNode', 'LedgerEntryType', 'OwnerNode',
|
||||||
|
'PreviousTxnID', 'PreviousTxnLgrSeq', 'Sequence', 'index'
|
||||||
|
]
|
||||||
|
for d in offers:
|
||||||
|
for dk in delete_keys:
|
||||||
|
del d[dk]
|
||||||
|
for t in ['TakerPays', 'TakerGets', 'owner_funds']:
|
||||||
|
if 'value' in d[t]:
|
||||||
|
d[t] = d[t]['value']
|
||||||
|
df = pd.DataFrame(offers)
|
||||||
|
df.rename(columns={
|
||||||
|
'Account': 'account',
|
||||||
|
'Flags': 'flags',
|
||||||
|
'TakerGets': 'taker_gets',
|
||||||
|
'TakerPays': 'taker_pays'
|
||||||
|
},
|
||||||
|
inplace=True)
|
||||||
|
return df
|
||||||
|
|
||||||
|
def account_balance(self, account: Account, asset: Asset) -> Asset:
|
||||||
|
'''get the account's balance of the asset'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
def substitute_nicknames(
|
||||||
|
self,
|
||||||
|
df: pd.DataFrame,
|
||||||
|
cols: List[str] = ['account', 'peer']) -> pd.DataFrame:
|
||||||
|
result = df.copy(deep=True)
|
||||||
|
for c in cols:
|
||||||
|
if c not in result:
|
||||||
|
continue
|
||||||
|
result[c] = result[c].map(
|
||||||
|
lambda x: self.key_manager.alias_or_account_id(x))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def add_to_keymanager(self, account: Account):
|
||||||
|
self.key_manager.add(account)
|
||||||
|
|
||||||
|
def is_alias(self, name: str) -> bool:
|
||||||
|
return self.key_manager.is_alias(name)
|
||||||
|
|
||||||
|
def account_from_alias(self, name: str) -> Account:
|
||||||
|
return self.key_manager.account_from_alias(name)
|
||||||
|
|
||||||
|
def known_accounts(self) -> List[Account]:
|
||||||
|
return self.key_manager.known_accounts()
|
||||||
|
|
||||||
|
def known_asset_aliases(self) -> List[str]:
|
||||||
|
return self.asset_aliases.known_aliases()
|
||||||
|
|
||||||
|
def known_iou_assets(self) -> List[Asset]:
|
||||||
|
return self.asset_aliases.known_assets()
|
||||||
|
|
||||||
|
def is_asset_alias(self, name: str) -> bool:
|
||||||
|
return self.asset_aliases.is_alias(name)
|
||||||
|
|
||||||
|
def add_asset_alias(self, asset: Asset, name: str):
|
||||||
|
self.asset_aliases.add(asset, name)
|
||||||
|
|
||||||
|
def asset_from_alias(self, name: str) -> Asset:
|
||||||
|
return self.asset_aliases.asset_from_alias(name)
|
||||||
|
|
||||||
|
def insert_seq_and_fee(self, txn: Transaction):
|
||||||
|
acc_info = self(AccountInfo(txn.account))
|
||||||
|
# TODO: set better fee (Hard code a fee of 15 for now)
|
||||||
|
txn.set_seq_and_fee(acc_info['account_data']['Sequence'], 15)
|
||||||
|
|
||||||
|
def get_client(self) -> RippleClient:
|
||||||
|
return self.client
|
||||||
|
|
||||||
|
|
||||||
|
def balances_dataframe(chains: List[App],
|
||||||
|
chain_names: List[str],
|
||||||
|
account_ids: Optional[List[Account]] = None,
|
||||||
|
assets: List[Asset] = None,
|
||||||
|
in_drops: bool = False):
|
||||||
|
def _removesuffix(self: str, suffix: str) -> str:
|
||||||
|
if suffix and self.endswith(suffix):
|
||||||
|
return self[:-len(suffix)]
|
||||||
|
else:
|
||||||
|
return self[:]
|
||||||
|
|
||||||
|
def _balance_df(chain: App, acc: Optional[Account],
|
||||||
|
asset: Union[Asset, List[Asset]], in_drops: bool):
|
||||||
|
b = chain.get_balances(acc, asset)
|
||||||
|
if not in_drops:
|
||||||
|
b.loc[b['currency'] == 'XRP', 'balance'] /= 1_000_000
|
||||||
|
b = chain.substitute_nicknames(b)
|
||||||
|
b = b.set_index('account')
|
||||||
|
return b
|
||||||
|
|
||||||
|
if account_ids is None:
|
||||||
|
account_ids = [None] * len(chains)
|
||||||
|
|
||||||
|
if assets is None:
|
||||||
|
# XRP and all assets in the assets alias list
|
||||||
|
assets = [[XRP(0)] + c.known_iou_assets() for c in chains]
|
||||||
|
|
||||||
|
dfs = []
|
||||||
|
keys = []
|
||||||
|
for chain, chain_name, acc, asset in zip(chains, chain_names, account_ids,
|
||||||
|
assets):
|
||||||
|
dfs.append(_balance_df(chain, acc, asset, in_drops))
|
||||||
|
keys.append(_removesuffix(chain_name, 'chain'))
|
||||||
|
df = pd.concat(dfs, keys=keys)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
# Start an app with a single client
|
||||||
|
@contextmanager
|
||||||
|
def single_client_app(*,
|
||||||
|
config: ConfigFile,
|
||||||
|
command_log: Optional[str] = None,
|
||||||
|
server_out=os.devnull,
|
||||||
|
run_server: bool = True,
|
||||||
|
exe: Optional[str] = None,
|
||||||
|
extra_args: Optional[List[str]] = None,
|
||||||
|
standalone=False):
|
||||||
|
'''Start a ripple server and return an app'''
|
||||||
|
try:
|
||||||
|
if extra_args is None:
|
||||||
|
extra_args = []
|
||||||
|
to_run = None
|
||||||
|
app = None
|
||||||
|
client = RippleClient(config=config, command_log=command_log, exe=exe)
|
||||||
|
if run_server:
|
||||||
|
to_run = [client.exe, '--conf', client.config_file_name]
|
||||||
|
if standalone:
|
||||||
|
to_run.append('-a')
|
||||||
|
fout = open(server_out, 'w')
|
||||||
|
p = subprocess.Popen(to_run + extra_args,
|
||||||
|
stdout=fout,
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
client.set_pid(p.pid)
|
||||||
|
print(
|
||||||
|
f'started rippled: config: {client.config_file_name} PID: {p.pid}',
|
||||||
|
flush=True)
|
||||||
|
time.sleep(1.5) # give process time to startup
|
||||||
|
app = App(client=client, standalone=standalone)
|
||||||
|
yield app
|
||||||
|
finally:
|
||||||
|
if app:
|
||||||
|
app.shutdown()
|
||||||
|
if run_server and to_run:
|
||||||
|
subprocess.Popen(to_run + ['stop'],
|
||||||
|
stdout=fout,
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
p.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def configs_for_testnet(config_file_prefix: str) -> List[ConfigFile]:
|
||||||
|
configs = []
|
||||||
|
p = Path(config_file_prefix)
|
||||||
|
dir = p.parent
|
||||||
|
file = p.name
|
||||||
|
file_names = []
|
||||||
|
for f in os.listdir(dir):
|
||||||
|
cfg = os.path.join(dir, f, 'rippled.cfg')
|
||||||
|
if f.startswith(file) and os.path.exists(cfg):
|
||||||
|
file_names.append(cfg)
|
||||||
|
file_names.sort()
|
||||||
|
return [ConfigFile(file_name=f) for f in file_names]
|
||||||
|
|
||||||
|
|
||||||
|
# Start an app for a network with the config files matched by `config_file_prefix*/rippled.cfg`
|
||||||
|
|
||||||
|
|
||||||
|
# Undocumented feature: if the environment variable RIPPLED_SIDECHAIN_RR is set, it is
|
||||||
|
# assumed to point to the rr executable. Sidechain server 0 will then be run under rr.
|
||||||
|
@contextmanager
|
||||||
|
def testnet_app(*,
|
||||||
|
exe: str,
|
||||||
|
configs: List[ConfigFile],
|
||||||
|
command_logs: Optional[List[str]] = None,
|
||||||
|
run_server: Optional[List[bool]] = None,
|
||||||
|
sidechain_rr: Optional[str] = None,
|
||||||
|
extra_args: Optional[List[List[str]]] = None):
|
||||||
|
'''Start a ripple testnet and return an app'''
|
||||||
|
try:
|
||||||
|
app = None
|
||||||
|
network = testnet.Network(exe,
|
||||||
|
configs,
|
||||||
|
command_logs=command_logs,
|
||||||
|
run_server=run_server,
|
||||||
|
with_rr=sidechain_rr,
|
||||||
|
extra_args=extra_args)
|
||||||
|
network.wait_for_validated_ledger()
|
||||||
|
app = App(network=network, standalone=False)
|
||||||
|
yield app
|
||||||
|
finally:
|
||||||
|
if app:
|
||||||
|
app.shutdown()
|
||||||
563
bin/sidechain/python/command.py
Normal file
563
bin/sidechain/python/command.py
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
import json
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
from common import Account, Asset
|
||||||
|
|
||||||
|
|
||||||
|
class Command:
|
||||||
|
'''Interface for all commands sent to the server'''
|
||||||
|
|
||||||
|
# command id useful for websocket messages
|
||||||
|
next_cmd_id = 1
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.cmd_id = Command.next_cmd_id
|
||||||
|
Command.next_cmd_id += 1
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
'''Return the command name for use in a command line'''
|
||||||
|
assert False
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def get_command_line_list(self) -> List[str]:
|
||||||
|
'''Return a list of strings suitable for a command line command for a rippled server'''
|
||||||
|
return [self.cmd_name, json.dumps(self.to_cmd_obj())]
|
||||||
|
|
||||||
|
def get_websocket_dict(self) -> dict:
|
||||||
|
'''Return a dictionary suitable for converting to json and sending to a rippled server using a websocket'''
|
||||||
|
result = self.to_cmd_obj()
|
||||||
|
return self.add_websocket_fields(result)
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
'''Return an object suitalbe for use in a command (input to json.dumps or similar)'''
|
||||||
|
assert False
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def add_websocket_fields(self, cmd_dict: dict) -> dict:
|
||||||
|
cmd_dict['id'] = self.cmd_id
|
||||||
|
cmd_dict['command'] = self.cmd_name()
|
||||||
|
return cmd_dict
|
||||||
|
|
||||||
|
def _set_flag(self, flag_bit: int, value: bool = True):
|
||||||
|
'''Set or clear the flag bit'''
|
||||||
|
if value:
|
||||||
|
self.flags |= flag_bit
|
||||||
|
else:
|
||||||
|
self.flags &= ~flag_bit
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionCommand(Command):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
|
||||||
|
class PathFind(Command):
|
||||||
|
'''Rippled ripple_path_find command'''
|
||||||
|
def __init__(self,
|
||||||
|
*,
|
||||||
|
src: Account,
|
||||||
|
dst: Account,
|
||||||
|
amt: Asset,
|
||||||
|
send_max: Optional[Asset] = None,
|
||||||
|
src_currencies: Optional[List[Asset]] = None,
|
||||||
|
ledger_hash: Optional[str] = None,
|
||||||
|
ledger_index: Optional[Union[int, str]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.src = src
|
||||||
|
self.dst = dst
|
||||||
|
self.amt = amt
|
||||||
|
self.send_max = send_max
|
||||||
|
self.src_currencies = src_currencies
|
||||||
|
self.ledger_hash = ledger_hash
|
||||||
|
self.ledger_index = ledger_index
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'ripple_path_find'
|
||||||
|
|
||||||
|
def add_websocket_fields(self, cmd_dict: dict) -> dict:
|
||||||
|
cmd_dict = super().add_websocket_fields(cmd_dict)
|
||||||
|
cmd_dict['subcommand'] = 'create'
|
||||||
|
return cmd_dict
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
'''convert to transaction form (suitable for using json.dumps or similar)'''
|
||||||
|
cmd = {
|
||||||
|
'source_account': self.src.account_id,
|
||||||
|
'destination_account': self.dst.account_id,
|
||||||
|
'destination_amount': self.amt.to_cmd_obj()
|
||||||
|
}
|
||||||
|
if self.send_max is not None:
|
||||||
|
cmd['send_max'] = self.send_max.to_cmd_obj()
|
||||||
|
if self.ledger_hash is not None:
|
||||||
|
cmd['ledger_hash'] = self.ledger_hash
|
||||||
|
if self.ledger_index is not None:
|
||||||
|
cmd['ledger_index'] = self.ledger_index
|
||||||
|
if self.src_currencies:
|
||||||
|
c = []
|
||||||
|
for sc in self.src_currencies:
|
||||||
|
d = {'currency': sc.currency, 'issuer': sc.issuer.account_id}
|
||||||
|
c.append(d)
|
||||||
|
cmd['source_currencies'] = c
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
class Sign(Command):
|
||||||
|
'''Rippled sign command'''
|
||||||
|
def __init__(self, secret: str, tx: dict):
|
||||||
|
super().__init__()
|
||||||
|
self.tx = tx
|
||||||
|
self.secret = secret
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'sign'
|
||||||
|
|
||||||
|
def get_command_line_list(self) -> List[str]:
|
||||||
|
'''Return a list of strings suitable for a command line command for a rippled server'''
|
||||||
|
return [self.cmd_name(), self.secret, f'{json.dumps(self.tx)}']
|
||||||
|
|
||||||
|
def get_websocket_dict(self) -> dict:
|
||||||
|
'''Return a dictionary suitable for converting to json and sending to a rippled server using a websocket'''
|
||||||
|
result = {'secret': self.secret, 'tx_json': self.tx}
|
||||||
|
return self.add_websocket_fields(result)
|
||||||
|
|
||||||
|
|
||||||
|
class Submit(Command):
|
||||||
|
'''Rippled submit command'''
|
||||||
|
def __init__(self, tx_blob: str):
|
||||||
|
super().__init__()
|
||||||
|
self.tx_blob = tx_blob
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'submit'
|
||||||
|
|
||||||
|
def get_command_line_list(self) -> List[str]:
|
||||||
|
'''Return a list of strings suitable for a command line command for a rippled server'''
|
||||||
|
return [self.cmd_name(), self.tx_blob]
|
||||||
|
|
||||||
|
def get_websocket_dict(self) -> dict:
|
||||||
|
'''Return a dictionary suitable for converting to json and sending to a rippled server using a websocket'''
|
||||||
|
result = {'tx_blob': self.tx_blob}
|
||||||
|
return self.add_websocket_fields(result)
|
||||||
|
|
||||||
|
|
||||||
|
class LedgerAccept(Command):
|
||||||
|
'''Rippled ledger_accept command'''
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'ledger_accept'
|
||||||
|
|
||||||
|
def get_command_line_list(self) -> List[str]:
|
||||||
|
'''Return a list of strings suitable for a command line command for a rippled server'''
|
||||||
|
return [self.cmd_name()]
|
||||||
|
|
||||||
|
def get_websocket_dict(self) -> dict:
|
||||||
|
'''Return a dictionary suitable for converting to json and sending to a rippled server using a websocket'''
|
||||||
|
result = {}
|
||||||
|
return self.add_websocket_fields(result)
|
||||||
|
|
||||||
|
|
||||||
|
class Stop(Command):
|
||||||
|
'''Rippled stop command'''
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'stop'
|
||||||
|
|
||||||
|
def get_command_line_list(self) -> List[str]:
|
||||||
|
'''Return a list of strings suitable for a command line command for a rippled server'''
|
||||||
|
return [self.cmd_name()]
|
||||||
|
|
||||||
|
def get_websocket_dict(self) -> dict:
|
||||||
|
'''Return a dictionary suitable for converting to json and sending to a rippled server using a websocket'''
|
||||||
|
result = {}
|
||||||
|
return self.add_websocket_fields(result)
|
||||||
|
|
||||||
|
|
||||||
|
class LogLevel(Command):
|
||||||
|
'''Rippled log_level command'''
|
||||||
|
def __init__(self, severity: str, *, partition: Optional[str] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.severity = severity
|
||||||
|
self.partition = partition
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'log_level'
|
||||||
|
|
||||||
|
def get_command_line_list(self) -> List[str]:
|
||||||
|
'''Return a list of strings suitable for a command line command for a rippled server'''
|
||||||
|
if self.partition is not None:
|
||||||
|
return [self.cmd_name(), self.partition, self.severity]
|
||||||
|
return [self.cmd_name(), self.severity]
|
||||||
|
|
||||||
|
def get_websocket_dict(self) -> dict:
|
||||||
|
'''Return a dictionary suitable for converting to json and sending to a rippled server using a websocket'''
|
||||||
|
result = {'severity': self.severity}
|
||||||
|
if self.partition is not None:
|
||||||
|
result['partition'] = self.partition
|
||||||
|
return self.add_websocket_fields(result)
|
||||||
|
|
||||||
|
|
||||||
|
class WalletPropose(Command):
|
||||||
|
'''Rippled wallet_propose command'''
|
||||||
|
def __init__(self,
|
||||||
|
*,
|
||||||
|
passphrase: Optional[str] = None,
|
||||||
|
seed: Optional[str] = None,
|
||||||
|
seed_hex: Optional[str] = None,
|
||||||
|
key_type: Optional[str] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.passphrase = passphrase
|
||||||
|
self.seed = seed
|
||||||
|
self.seed_hex = seed_hex
|
||||||
|
self.key_type = key_type
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'wallet_propose'
|
||||||
|
|
||||||
|
def get_command_line_list(self) -> List[str]:
|
||||||
|
'''Return a list of strings suitable for a command line command for a rippled server'''
|
||||||
|
assert not self.seed and not self.seed_hex and (
|
||||||
|
not self.key_type or self.key_type == 'secp256k1')
|
||||||
|
if self.passphrase:
|
||||||
|
return [self.cmd_name(), self.passphrase]
|
||||||
|
return [self.cmd_name()]
|
||||||
|
|
||||||
|
def get_websocket_dict(self) -> dict:
|
||||||
|
'''Return a dictionary suitable for converting to json and sending to a rippled server using a websocket'''
|
||||||
|
result = {}
|
||||||
|
if self.seed is not None:
|
||||||
|
result['seed'] = self.seed
|
||||||
|
if self.seed_hex is not None:
|
||||||
|
result['seed_hex'] = self.seed_hex
|
||||||
|
if self.passphrase is not None:
|
||||||
|
result['passphrase'] = self.passphrase
|
||||||
|
if self.key_type is not None:
|
||||||
|
result['key_type'] = self.key_type
|
||||||
|
return self.add_websocket_fields(result)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationCreate(Command):
|
||||||
|
'''Rippled validation_create command'''
|
||||||
|
def __init__(self, *, secret: Optional[str] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.secret = secret
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'validation_create'
|
||||||
|
|
||||||
|
def get_command_line_list(self) -> List[str]:
|
||||||
|
'''Return a list of strings suitable for a command line command for a rippled server'''
|
||||||
|
if self.secret:
|
||||||
|
return [self.cmd_name(), self.secret]
|
||||||
|
return [self.cmd_name()]
|
||||||
|
|
||||||
|
def get_websocket_dict(self) -> dict:
|
||||||
|
'''Return a dictionary suitable for converting to json and sending to a rippled server using a websocket'''
|
||||||
|
result = {}
|
||||||
|
if self.secret is not None:
|
||||||
|
result['secret'] = self.secret
|
||||||
|
return self.add_websocket_fields(result)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountInfo(Command):
|
||||||
|
'''Rippled account_info command'''
|
||||||
|
def __init__(self,
|
||||||
|
account: Account,
|
||||||
|
*,
|
||||||
|
strict: Optional[bool] = None,
|
||||||
|
ledger_hash: Optional[str] = None,
|
||||||
|
ledger_index: Optional[Union[str, int]] = None,
|
||||||
|
queue: Optional[bool] = None,
|
||||||
|
signers_list: Optional[bool] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.account = account
|
||||||
|
self.strict = strict
|
||||||
|
self.ledger_hash = ledger_hash
|
||||||
|
self.ledger_index = ledger_index
|
||||||
|
self.queue = queue
|
||||||
|
self.signers_list = signers_list
|
||||||
|
assert not ((ledger_hash is not None) and (ledger_index is not None))
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'account_info'
|
||||||
|
|
||||||
|
def get_command_line_list(self) -> List[str]:
|
||||||
|
'''Return a list of strings suitable for a command line command for a rippled server'''
|
||||||
|
result = [self.cmd_name(), self.account.account_id]
|
||||||
|
if self.ledger_index is not None:
|
||||||
|
result.append(self.ledger_index)
|
||||||
|
if self.ledger_hash is not None:
|
||||||
|
result.append(self.ledger_hash)
|
||||||
|
if self.strict is not None:
|
||||||
|
result.append(self.strict)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_websocket_dict(self) -> dict:
|
||||||
|
'''Return a dictionary suitable for converting to json and sending to a rippled server using a websocket'''
|
||||||
|
result = {'account': self.account.account_id}
|
||||||
|
if self.ledger_index is not None:
|
||||||
|
result['ledger_index'] = self.ledger_index
|
||||||
|
if self.ledger_hash is not None:
|
||||||
|
result['ledger_hash'] = self.ledger_hash
|
||||||
|
if self.strict is not None:
|
||||||
|
result['strict'] = self.strict
|
||||||
|
if self.queue is not None:
|
||||||
|
result['queue'] = self.queue
|
||||||
|
return self.add_websocket_fields(result)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountLines(Command):
|
||||||
|
'''Rippled account_lines command'''
|
||||||
|
def __init__(self,
|
||||||
|
account: Account,
|
||||||
|
*,
|
||||||
|
peer: Optional[Account] = None,
|
||||||
|
ledger_hash: Optional[str] = None,
|
||||||
|
ledger_index: Optional[Union[str, int]] = None,
|
||||||
|
limit: Optional[int] = None,
|
||||||
|
marker=None):
|
||||||
|
super().__init__()
|
||||||
|
self.account = account
|
||||||
|
self.peer = peer
|
||||||
|
self.ledger_hash = ledger_hash
|
||||||
|
self.ledger_index = ledger_index
|
||||||
|
self.limit = limit
|
||||||
|
self.marker = marker
|
||||||
|
assert not ((ledger_hash is not None) and (ledger_index is not None))
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'account_lines'
|
||||||
|
|
||||||
|
def get_command_line_list(self) -> List[str]:
|
||||||
|
'''Return a list of strings suitable for a command line command for a rippled server'''
|
||||||
|
assert sum(x is None for x in [
|
||||||
|
self.ledger_index, self.ledger_hash, self.limit, self.marker
|
||||||
|
]) == 4
|
||||||
|
result = [self.cmd_name(), self.account.account_id]
|
||||||
|
if self.peer is not None:
|
||||||
|
result.append(self.peer)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_websocket_dict(self) -> dict:
|
||||||
|
'''Return a dictionary suitable for converting to json and sending to a rippled server using a websocket'''
|
||||||
|
result = {'account': self.account.account_id}
|
||||||
|
if self.peer is not None:
|
||||||
|
result['peer'] = self.peer
|
||||||
|
if self.ledger_index is not None:
|
||||||
|
result['ledger_index'] = self.ledger_index
|
||||||
|
if self.ledger_hash is not None:
|
||||||
|
result['ledger_hash'] = self.ledger_hash
|
||||||
|
if self.limit is not None:
|
||||||
|
result['limit'] = self.limit
|
||||||
|
if self.marker is not None:
|
||||||
|
result['marker'] = self.marker
|
||||||
|
return self.add_websocket_fields(result)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountTx(Command):
|
||||||
|
def __init__(self,
|
||||||
|
account: Account,
|
||||||
|
*,
|
||||||
|
limit: Optional[int] = None,
|
||||||
|
marker=None):
|
||||||
|
super().__init__()
|
||||||
|
self.account = account
|
||||||
|
self.limit = limit
|
||||||
|
self.marker = marker
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'account_tx'
|
||||||
|
|
||||||
|
def get_command_line_list(self) -> List[str]:
|
||||||
|
'''Return a list of strings suitable for a command line command for a rippled server'''
|
||||||
|
result = [self.cmd_name(), self.account.account_id]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_websocket_dict(self) -> dict:
|
||||||
|
'''Return a dictionary suitable for converting to json and sending to a rippled server using a websocket'''
|
||||||
|
result = {'account': self.account.account_id}
|
||||||
|
if self.limit is not None:
|
||||||
|
result['limit'] = self.limit
|
||||||
|
if self.marker is not None:
|
||||||
|
result['marker'] = self.marker
|
||||||
|
return self.add_websocket_fields(result)
|
||||||
|
|
||||||
|
|
||||||
|
class BookOffers(Command):
|
||||||
|
'''Rippled book_offers command'''
|
||||||
|
def __init__(self,
|
||||||
|
taker_pays: Asset,
|
||||||
|
taker_gets: Asset,
|
||||||
|
*,
|
||||||
|
taker: Optional[Account] = None,
|
||||||
|
ledger_hash: Optional[str] = None,
|
||||||
|
ledger_index: Optional[Union[str, int]] = None,
|
||||||
|
limit: Optional[int] = None,
|
||||||
|
marker=None):
|
||||||
|
super().__init__()
|
||||||
|
self.taker_pays = taker_pays
|
||||||
|
self.taker_gets = taker_gets
|
||||||
|
self.taker = taker
|
||||||
|
self.ledger_hash = ledger_hash
|
||||||
|
self.ledger_index = ledger_index
|
||||||
|
self.limit = limit
|
||||||
|
self.marker = marker
|
||||||
|
assert not ((ledger_hash is not None) and (ledger_index is not None))
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'book_offers'
|
||||||
|
|
||||||
|
def get_command_line_list(self) -> List[str]:
|
||||||
|
'''Return a list of strings suitable for a command line command for a rippled server'''
|
||||||
|
assert sum(x is None for x in [
|
||||||
|
self.ledger_index, self.ledger_hash, self.limit, self.marker
|
||||||
|
]) == 4
|
||||||
|
return [
|
||||||
|
self.cmd_name(),
|
||||||
|
self.taker_pays.cmd_str(),
|
||||||
|
self.taker_gets.cmd_str()
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_websocket_dict(self) -> dict:
|
||||||
|
'''Return a dictionary suitable for converting to json and sending to a rippled server using a websocket'''
|
||||||
|
result = {
|
||||||
|
'taker_pays': self.taker_pays.to_cmd_obj(),
|
||||||
|
'taker_gets': self.taker_gets.to_cmd_obj()
|
||||||
|
}
|
||||||
|
if self.taker is not None:
|
||||||
|
result['taker'] = self.taker.account_id
|
||||||
|
if self.ledger_index is not None:
|
||||||
|
result['ledger_index'] = self.ledger_index
|
||||||
|
if self.ledger_hash is not None:
|
||||||
|
result['ledger_hash'] = self.ledger_hash
|
||||||
|
if self.limit is not None:
|
||||||
|
result['limit'] = self.limit
|
||||||
|
if self.marker is not None:
|
||||||
|
result['marker'] = self.marker
|
||||||
|
return self.add_websocket_fields(result)
|
||||||
|
|
||||||
|
|
||||||
|
class BookSubscription:
|
||||||
|
'''Spec for a book in a subscribe command'''
|
||||||
|
def __init__(self,
|
||||||
|
taker_pays: Asset,
|
||||||
|
taker_gets: Asset,
|
||||||
|
*,
|
||||||
|
taker: Optional[Account] = None,
|
||||||
|
snapshot: Optional[bool] = None,
|
||||||
|
both: Optional[bool] = None):
|
||||||
|
self.taker_pays = taker_pays
|
||||||
|
self.taker_gets = taker_gets
|
||||||
|
self.taker = taker
|
||||||
|
self.snapshot = snapshot
|
||||||
|
self.both = both
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
'''Return an object suitalbe for use in a command'''
|
||||||
|
result = {
|
||||||
|
'taker_pays': self.taker_pays.to_cmd_obj(),
|
||||||
|
'taker_gets': self.taker_gets.to_cmd_obj()
|
||||||
|
}
|
||||||
|
if self.taker is not None:
|
||||||
|
result['taker'] = self.taker.account_id
|
||||||
|
if self.snapshot is not None:
|
||||||
|
result['snapshot'] = self.snapshot
|
||||||
|
if self.both is not None:
|
||||||
|
result['both'] = self.both
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ServerInfo(Command):
|
||||||
|
'''Rippled server_info command'''
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'server_info'
|
||||||
|
|
||||||
|
def get_command_line_list(self) -> List[str]:
|
||||||
|
'''Return a list of strings suitable for a command line command for a rippled server'''
|
||||||
|
return [self.cmd_name()]
|
||||||
|
|
||||||
|
def get_websocket_dict(self) -> dict:
|
||||||
|
'''Return a dictionary suitable for converting to json and sending to a rippled server using a websocket'''
|
||||||
|
result = {}
|
||||||
|
return self.add_websocket_fields(result)
|
||||||
|
|
||||||
|
|
||||||
|
class FederatorInfo(Command):
|
||||||
|
'''Rippled federator_info command'''
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'federator_info'
|
||||||
|
|
||||||
|
def get_command_line_list(self) -> List[str]:
|
||||||
|
'''Return a list of strings suitable for a command line command for a rippled server'''
|
||||||
|
return [self.cmd_name()]
|
||||||
|
|
||||||
|
def get_websocket_dict(self) -> dict:
|
||||||
|
'''Return a dictionary suitable for converting to json and sending to a rippled server using a websocket'''
|
||||||
|
result = {}
|
||||||
|
return self.add_websocket_fields(result)
|
||||||
|
|
||||||
|
|
||||||
|
class Subscribe(SubscriptionCommand):
|
||||||
|
'''The subscribe method requests periodic notifications from the server
|
||||||
|
when certain events happen. See: https://developers.ripple.com/subscribe.html'''
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
streams: Optional[List[str]] = None,
|
||||||
|
accounts: Optional[List[Account]] = None,
|
||||||
|
accounts_proposed: Optional[List[Account]] = None,
|
||||||
|
account_history_account: Optional[Account] = None,
|
||||||
|
books: Optional[
|
||||||
|
List[BookSubscription]] = None, # taker_pays, taker_gets
|
||||||
|
url: Optional[str] = None,
|
||||||
|
url_username: Optional[str] = None,
|
||||||
|
url_password: Optional[str] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.streams = streams
|
||||||
|
self.accounts = accounts
|
||||||
|
self.account_history_account = account_history_account
|
||||||
|
self.accounts_proposed = accounts_proposed
|
||||||
|
self.books = books
|
||||||
|
self.url = url
|
||||||
|
self.url_username = url_username
|
||||||
|
self.url_password = url_password
|
||||||
|
self.websocket = None
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
if self.websocket:
|
||||||
|
return 'unsubscribe'
|
||||||
|
return 'subscribe'
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
d = {}
|
||||||
|
if self.streams is not None:
|
||||||
|
d['streams'] = self.streams
|
||||||
|
if self.accounts is not None:
|
||||||
|
d['accounts'] = [a.account_id for a in self.accounts]
|
||||||
|
if self.account_history_account is not None:
|
||||||
|
d['account_history_tx_stream'] = {
|
||||||
|
'account': self.account_history_account.account_id
|
||||||
|
}
|
||||||
|
if self.accounts_proposed is not None:
|
||||||
|
d['accounts_proposed'] = [
|
||||||
|
a.account_id for a in self.accounts_proposed
|
||||||
|
]
|
||||||
|
if self.books is not None:
|
||||||
|
d['books'] = [b.to_cmd_obj() for b in self.books]
|
||||||
|
if self.url is not None:
|
||||||
|
d['url'] = self.url
|
||||||
|
if self.url_username is not None:
|
||||||
|
d['url_username'] = self.url_username
|
||||||
|
if self.url_password is not None:
|
||||||
|
d['url_password'] = self.url_password
|
||||||
|
return d
|
||||||
256
bin/sidechain/python/common.py
Normal file
256
bin/sidechain/python/common.py
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import binascii
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
import pandas as pd
|
||||||
|
import pytz
|
||||||
|
import sys
|
||||||
|
|
||||||
|
EPRINT_ENABLED = True
|
||||||
|
|
||||||
|
|
||||||
|
def disable_eprint():
|
||||||
|
global EPRINT_ENABLED
|
||||||
|
EPRINT_ENABLED = False
|
||||||
|
|
||||||
|
|
||||||
|
def enable_eprint():
|
||||||
|
global EPRINT_ENABLED
|
||||||
|
EPRINT_ENABLED = True
|
||||||
|
|
||||||
|
|
||||||
|
def eprint(*args, **kwargs):
|
||||||
|
if not EPRINT_ENABLED:
|
||||||
|
return
|
||||||
|
logging.error(*args)
|
||||||
|
print(*args, file=sys.stderr, flush=True, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def to_rippled_epoch(d: datetime.datetime) -> int:
|
||||||
|
'''Convert from a datetime to the number of seconds since Jan 1, 2000 (rippled epoch)'''
|
||||||
|
start = datetime.datetime(2000, 1, 1, tzinfo=pytz.utc)
|
||||||
|
return int((d - start).total_seconds())
|
||||||
|
|
||||||
|
|
||||||
|
class Account: # pylint: disable=too-few-public-methods
|
||||||
|
'''
|
||||||
|
Account in the ripple ledger
|
||||||
|
'''
|
||||||
|
def __init__(self,
|
||||||
|
*,
|
||||||
|
account_id: Optional[str] = None,
|
||||||
|
nickname: Optional[str] = None,
|
||||||
|
public_key: Optional[str] = None,
|
||||||
|
public_key_hex: Optional[str] = None,
|
||||||
|
secret_key: Optional[str] = None,
|
||||||
|
result_dict: Optional[dict] = None):
|
||||||
|
self.account_id = account_id
|
||||||
|
self.nickname = nickname
|
||||||
|
self.public_key = public_key
|
||||||
|
self.public_key_hex = public_key_hex
|
||||||
|
self.secret_key = secret_key
|
||||||
|
|
||||||
|
if result_dict is not None:
|
||||||
|
self.account_id = result_dict['account_id']
|
||||||
|
self.public_key = result_dict['public_key']
|
||||||
|
self.public_key_hex = result_dict['public_key_hex']
|
||||||
|
self.secret_key = result_dict['master_seed']
|
||||||
|
|
||||||
|
# Accounts are equal if they represent the same account on the ledger
|
||||||
|
# I.e. only check the account_id field for equality.
|
||||||
|
def __eq__(self, lhs):
|
||||||
|
if not isinstance(lhs, self.__class__):
|
||||||
|
return False
|
||||||
|
return self.account_id == lhs.account_id
|
||||||
|
|
||||||
|
def __ne__(self, lhs):
|
||||||
|
return not self.__eq__(lhs)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if self.nickname is not None:
|
||||||
|
return self.nickname
|
||||||
|
return self.account_id
|
||||||
|
|
||||||
|
def alias_or_account_id(self) -> str:
|
||||||
|
'''
|
||||||
|
return the alias if it exists, otherwise return the id
|
||||||
|
'''
|
||||||
|
if self.nickname is not None:
|
||||||
|
return self.nickname
|
||||||
|
return self.account_id
|
||||||
|
|
||||||
|
def account_id_str_as_hex(self) -> str:
|
||||||
|
return binascii.hexlify(self.account_id.encode()).decode('utf-8')
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
return {
|
||||||
|
'account_id': self.account_id,
|
||||||
|
'nickname': self.nickname,
|
||||||
|
'public_key': self.public_key,
|
||||||
|
'public_key_hex': self.public_key_hex,
|
||||||
|
'secret_key': self.secret_key
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Asset:
|
||||||
|
'''An XRP or IOU value'''
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
value: Union[int, float, None] = None,
|
||||||
|
currency: Optional[
|
||||||
|
str] = None, # Will default to 'XRP' if not specified
|
||||||
|
issuer: Optional[Account] = None,
|
||||||
|
from_asset=None, # asset is of type Optional[Asset]
|
||||||
|
# from_rpc_result is a python object resulting from an rpc command
|
||||||
|
from_rpc_result: Optional[Union[dict, str]] = None):
|
||||||
|
|
||||||
|
assert from_asset is None or from_rpc_result is None
|
||||||
|
|
||||||
|
self.value = value
|
||||||
|
self.issuer = issuer
|
||||||
|
self.currency = currency
|
||||||
|
if from_asset is not None:
|
||||||
|
if self.value is None:
|
||||||
|
self.value = from_asset.value
|
||||||
|
if self.issuer is None:
|
||||||
|
self.issuer = from_asset.issuer
|
||||||
|
if self.currency is None:
|
||||||
|
self.currency = from_asset.currency
|
||||||
|
if from_rpc_result is not None:
|
||||||
|
if isinstance(from_rpc_result, str):
|
||||||
|
self.value = int(from_rpc_result)
|
||||||
|
self.currency = 'XRP'
|
||||||
|
else:
|
||||||
|
self.value = from_rpc_result['value']
|
||||||
|
self.currency = float(from_rpc_result['currency'])
|
||||||
|
self.issuer = Account(account_id=from_rpc_result['issuer'])
|
||||||
|
|
||||||
|
if self.currency is None:
|
||||||
|
self.currency = 'XRP'
|
||||||
|
|
||||||
|
if isinstance(self.value, str):
|
||||||
|
if self.is_xrp():
|
||||||
|
self.value = int(value)
|
||||||
|
else:
|
||||||
|
self.value = float(value)
|
||||||
|
|
||||||
|
def __call__(self, value: Union[int, float]):
|
||||||
|
'''Call operator useful for a terse syntax for assets in tests. I.e. USD(100)'''
|
||||||
|
return Asset(value=value, from_asset=self)
|
||||||
|
|
||||||
|
def __add__(self, lhs):
|
||||||
|
assert (self.issuer == lhs.issuer and self.currency == lhs.currency)
|
||||||
|
return Asset(value=self.value + lhs.value,
|
||||||
|
currency=self.currency,
|
||||||
|
issuer=self.issuer)
|
||||||
|
|
||||||
|
def __sub__(self, lhs):
|
||||||
|
assert (self.issuer == lhs.issuer and self.currency == lhs.currency)
|
||||||
|
return Asset(value=self.value - lhs.value,
|
||||||
|
currency=self.currency,
|
||||||
|
issuer=self.issuer)
|
||||||
|
|
||||||
|
def __eq__(self, lhs):
|
||||||
|
if not isinstance(lhs, self.__class__):
|
||||||
|
return False
|
||||||
|
return (self.value == lhs.value and self.currency == lhs.currency
|
||||||
|
and self.issuer == lhs.issuer)
|
||||||
|
|
||||||
|
def __ne__(self, lhs):
|
||||||
|
return not self.__eq__(lhs)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
value_part = '' if self.value is None else f'{self.value}/'
|
||||||
|
issuer_part = '' if self.issuer is None else f'/{self.issuer}'
|
||||||
|
return f'{value_part}{self.currency}{issuer_part}'
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
def is_xrp(self) -> bool:
|
||||||
|
''' return true if the asset represents XRP'''
|
||||||
|
return self.currency == 'XRP'
|
||||||
|
|
||||||
|
def cmd_str(self) -> str:
|
||||||
|
value_part = '' if self.value is None else f'{self.value}/'
|
||||||
|
issuer_part = '' if self.issuer is None else f'/{self.issuer.account_id}'
|
||||||
|
return f'{value_part}{self.currency}{issuer_part}'
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
'''Return an object suitalbe for use in a command'''
|
||||||
|
if self.currency == 'XRP':
|
||||||
|
if self.value is not None:
|
||||||
|
return f'{self.value}' # must be a string
|
||||||
|
return {'currency': self.currency}
|
||||||
|
result = {'currency': self.currency, 'issuer': self.issuer.account_id}
|
||||||
|
if self.value is not None:
|
||||||
|
result['value'] = f'{self.value}' # must be a string
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def XRP(v: Union[int, float]) -> Asset:
|
||||||
|
return Asset(value=v * 1_000_000)
|
||||||
|
|
||||||
|
|
||||||
|
def drops(v: int) -> Asset:
|
||||||
|
return Asset(value=v)
|
||||||
|
|
||||||
|
|
||||||
|
class Path:
|
||||||
|
'''Payment Path'''
|
||||||
|
def __init__(self,
|
||||||
|
nodes: Optional[List[Union[Account, Asset]]] = None,
|
||||||
|
*,
|
||||||
|
result_list: Optional[List[dict]] = None):
|
||||||
|
assert not (nodes and result_list)
|
||||||
|
if result_list is not None:
|
||||||
|
self.result_list = result_list
|
||||||
|
return
|
||||||
|
if nodes is None:
|
||||||
|
self.result_list = []
|
||||||
|
return
|
||||||
|
self.result_list = [
|
||||||
|
self._create_account_path_node(n)
|
||||||
|
if isinstance(n, Account) else self._create_currency_path_node(n)
|
||||||
|
for n in nodes
|
||||||
|
]
|
||||||
|
|
||||||
|
def _create_account_path_node(self, account: Account) -> dict:
|
||||||
|
return {
|
||||||
|
'account': account.account_id,
|
||||||
|
'type': 1,
|
||||||
|
'type_hex': '0000000000000001'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _create_currency_path_node(self, asset: Asset) -> dict:
|
||||||
|
result = {
|
||||||
|
'currency': asset.currency,
|
||||||
|
'type': 48,
|
||||||
|
'type_hex': '0000000000000030'
|
||||||
|
}
|
||||||
|
if not asset.is_xrp():
|
||||||
|
result['issuer'] = asset.issuer.account_id
|
||||||
|
return result
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> list:
|
||||||
|
'''Return an object suitalbe for use in a command'''
|
||||||
|
return self.result_list
|
||||||
|
|
||||||
|
|
||||||
|
class PathList:
|
||||||
|
'''Collection of paths for use in payments'''
|
||||||
|
def __init__(self,
|
||||||
|
path_list: Optional[List[Path]] = None,
|
||||||
|
*,
|
||||||
|
result_list: Optional[List[List[dict]]] = None):
|
||||||
|
# result_list can be the response from the rippled server
|
||||||
|
assert not (path_list and result_list)
|
||||||
|
if result_list is not None:
|
||||||
|
self.paths = [Path(result_list=l) for l in result_list]
|
||||||
|
return
|
||||||
|
self.paths = path_list
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> list:
|
||||||
|
'''Return an object suitalbe for use in a command'''
|
||||||
|
return [p.to_cmd_obj() for p in self.paths]
|
||||||
101
bin/sidechain/python/config_file.py
Normal file
101
bin/sidechain/python/config_file.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class Section:
|
||||||
|
def section_header(l: str) -> Optional[str]:
|
||||||
|
'''
|
||||||
|
If the line is a section header, return the section name
|
||||||
|
otherwise return None
|
||||||
|
'''
|
||||||
|
if l.startswith('[') and l.endswith(']'):
|
||||||
|
return l[1:-1]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __init__(self, name: str):
|
||||||
|
super().__setattr__('_name', name)
|
||||||
|
# lines contains all non key-value pairs
|
||||||
|
super().__setattr__('_lines', [])
|
||||||
|
super().__setattr__('_kv_pairs', {})
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
def add_line(self, l):
|
||||||
|
s = l.split('=')
|
||||||
|
if len(s) == 2:
|
||||||
|
self._kv_pairs[s[0].strip()] = s[1].strip()
|
||||||
|
else:
|
||||||
|
self._lines.append(l)
|
||||||
|
|
||||||
|
def get_lines(self):
|
||||||
|
return self._lines
|
||||||
|
|
||||||
|
def get_line(self) -> Optional[str]:
|
||||||
|
if len(self._lines) > 0:
|
||||||
|
return self._lines[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
try:
|
||||||
|
return self._kv_pairs[name]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(name)
|
||||||
|
|
||||||
|
def __setattr__(self, name, value):
|
||||||
|
if name in self.__dict__:
|
||||||
|
super().__setattr__(name, value)
|
||||||
|
else:
|
||||||
|
self._kv_pairs[name] = value
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
return vars(self)
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
vars(self).update(state)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFile:
|
||||||
|
def __init__(self, *, file_name: Optional[str] = None):
|
||||||
|
# parse the file
|
||||||
|
self._file_name = file_name
|
||||||
|
self._sections = {}
|
||||||
|
if not file_name:
|
||||||
|
return
|
||||||
|
|
||||||
|
cur_section = None
|
||||||
|
with open(file_name) as f:
|
||||||
|
for n, l in enumerate(f):
|
||||||
|
l = l.strip()
|
||||||
|
if l.startswith('#') or not l:
|
||||||
|
continue
|
||||||
|
if section_name := Section.section_header(l):
|
||||||
|
if cur_section:
|
||||||
|
self.add_section(cur_section)
|
||||||
|
cur_section = Section(section_name)
|
||||||
|
continue
|
||||||
|
if not cur_section:
|
||||||
|
raise ValueError(
|
||||||
|
f'Error parsing config file: {file_name} line_num: {n} line: {l}'
|
||||||
|
)
|
||||||
|
cur_section.add_line(l)
|
||||||
|
|
||||||
|
if cur_section:
|
||||||
|
self.add_section(cur_section)
|
||||||
|
|
||||||
|
def add_section(self, s: Section):
|
||||||
|
self._sections[s.get_name()] = s
|
||||||
|
|
||||||
|
def get_file_name(self):
|
||||||
|
return self._file_name
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
return vars(self)
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
vars(self).update(state)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
try:
|
||||||
|
return self._sections[name]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(name)
|
||||||
630
bin/sidechain/python/create_config_files.py
Executable file
630
bin/sidechain/python/create_config_files.py
Executable file
@@ -0,0 +1,630 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Generate rippled config files, each with their own ports, database paths, and validation_seeds.
|
||||||
|
# There will be configs for shards/no_shards, main/test nets, two config files for each combination
|
||||||
|
# (so one can run in a dogfood mode while another is tested). To avoid confusion,The directory path
|
||||||
|
# will be $data_dir/{main | test}.{shard | no_shard}.{dog | test}
|
||||||
|
# The config file will reside in that directory with the name rippled.cfg
|
||||||
|
# The validators file will reside in that directory with the name validators.txt
|
||||||
|
'''
|
||||||
|
Script to test and debug sidechains.
|
||||||
|
|
||||||
|
The rippled exe location can be set through the command line or
|
||||||
|
the environment variable RIPPLED_MAINCHAIN_EXE
|
||||||
|
|
||||||
|
The configs_dir (where the config files will reside) can be set through the command line
|
||||||
|
or the environment variable RIPPLED_SIDECHAIN_CFG_DIR
|
||||||
|
'''
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
from typing import Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from config_file import ConfigFile
|
||||||
|
from command import ValidationCreate, WalletPropose
|
||||||
|
from common import Account, Asset, eprint, XRP
|
||||||
|
from app import App, single_client_app
|
||||||
|
|
||||||
|
mainnet_validators = """
|
||||||
|
[validator_list_sites]
|
||||||
|
https://vl.ripple.com
|
||||||
|
|
||||||
|
[validator_list_keys]
|
||||||
|
ED2677ABFFD1B33AC6FBC3062B71F1E8397C1505E1C42C64D11AD1B28FF73F4734
|
||||||
|
"""
|
||||||
|
|
||||||
|
altnet_validators = """
|
||||||
|
[validator_list_sites]
|
||||||
|
https://vl.altnet.rippletest.net
|
||||||
|
|
||||||
|
[validator_list_keys]
|
||||||
|
ED264807102805220DA0F312E71FC2C69E1552C9C5790F6C25E3729DEB573D5860
|
||||||
|
"""
|
||||||
|
|
||||||
|
node_size = 'medium'
|
||||||
|
default_data_dir = '/home/swd/data/rippled'
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Keypair:
|
||||||
|
public_key: str
|
||||||
|
secret_key: str
|
||||||
|
account_id: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_node_keypairs(n: int, rip: App) -> List[Keypair]:
|
||||||
|
'''
|
||||||
|
generate keypairs suitable for validator keys
|
||||||
|
'''
|
||||||
|
result = []
|
||||||
|
for i in range(n):
|
||||||
|
keys = rip(ValidationCreate())
|
||||||
|
result.append(
|
||||||
|
Keypair(public_key=keys["validation_public_key"],
|
||||||
|
secret_key=keys["validation_seed"],
|
||||||
|
account_id=None))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def generate_federator_keypairs(n: int, rip: App) -> List[Keypair]:
|
||||||
|
'''
|
||||||
|
generate keypairs suitable for federator keys
|
||||||
|
'''
|
||||||
|
result = []
|
||||||
|
for i in range(n):
|
||||||
|
keys = rip(WalletPropose(key_type='ed25519'))
|
||||||
|
result.append(
|
||||||
|
Keypair(public_key=keys["public_key"],
|
||||||
|
secret_key=keys["master_seed"],
|
||||||
|
account_id=keys["account_id"]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class Ports:
|
||||||
|
'''
|
||||||
|
Port numbers for various services.
|
||||||
|
Port numbers differ by cfg_index so different configs can run
|
||||||
|
at the same time without interfering with each other.
|
||||||
|
'''
|
||||||
|
peer_port_base = 51235
|
||||||
|
http_admin_port_base = 5005
|
||||||
|
ws_public_port_base = 6005
|
||||||
|
|
||||||
|
def __init__(self, cfg_index: int):
|
||||||
|
self.peer_port = Ports.peer_port_base + cfg_index
|
||||||
|
self.http_admin_port = Ports.http_admin_port_base + cfg_index
|
||||||
|
self.ws_public_port = Ports.ws_public_port_base + (2 * cfg_index)
|
||||||
|
# note admin port uses public port base
|
||||||
|
self.ws_admin_port = Ports.ws_public_port_base + (2 * cfg_index) + 1
|
||||||
|
|
||||||
|
|
||||||
|
class Network:
|
||||||
|
def __init__(self, num_nodes: int, num_validators: int,
|
||||||
|
start_cfg_index: int, rip: App):
|
||||||
|
self.validator_keypairs = generate_node_keypairs(num_validators, rip)
|
||||||
|
self.ports = [Ports(start_cfg_index + i) for i in range(num_nodes)]
|
||||||
|
|
||||||
|
|
||||||
|
class SidechainNetwork(Network):
|
||||||
|
def __init__(self, num_nodes: int, num_federators: int,
|
||||||
|
num_validators: int, start_cfg_index: int, rip: App):
|
||||||
|
super().__init__(num_nodes, num_validators, start_cfg_index, rip)
|
||||||
|
self.federator_keypairs = generate_federator_keypairs(
|
||||||
|
num_federators, rip)
|
||||||
|
self.main_account = rip(WalletPropose(key_type='secp256k1'))
|
||||||
|
|
||||||
|
|
||||||
|
class XChainAsset:
|
||||||
|
def __init__(self, main_asset: Asset, side_asset: Asset,
|
||||||
|
main_value: Union[int, float], side_value: Union[int, float],
|
||||||
|
main_refund_penalty: Union[int, float],
|
||||||
|
side_refund_penalty: Union[int, float]):
|
||||||
|
self.main_asset = main_asset(main_value)
|
||||||
|
self.side_asset = side_asset(side_value)
|
||||||
|
self.main_refund_penalty = main_asset(main_refund_penalty)
|
||||||
|
self.side_refund_penalty = side_asset(side_refund_penalty)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_asset_stanzas(
|
||||||
|
assets: Optional[Dict[str, XChainAsset]] = None) -> str:
|
||||||
|
if assets is None:
|
||||||
|
# default to xrp only at a 1:1 value
|
||||||
|
assets = {}
|
||||||
|
assets['xrp_xrp_sidechain_asset'] = XChainAsset(
|
||||||
|
XRP(0), XRP(0), 1, 1, 400, 400)
|
||||||
|
|
||||||
|
index_stanza = """
|
||||||
|
[sidechain_assets]"""
|
||||||
|
|
||||||
|
asset_stanzas = []
|
||||||
|
|
||||||
|
for name, xchainasset in assets.items():
|
||||||
|
index_stanza += '\n' + name
|
||||||
|
new_stanza = f"""
|
||||||
|
[{name}]
|
||||||
|
mainchain_asset={json.dumps(xchainasset.main_asset.to_cmd_obj())}
|
||||||
|
sidechain_asset={json.dumps(xchainasset.side_asset.to_cmd_obj())}
|
||||||
|
mainchain_refund_penalty={json.dumps(xchainasset.main_refund_penalty.to_cmd_obj())}
|
||||||
|
sidechain_refund_penalty={json.dumps(xchainasset.side_refund_penalty.to_cmd_obj())}"""
|
||||||
|
asset_stanzas.append(new_stanza)
|
||||||
|
|
||||||
|
return index_stanza + '\n' + '\n'.join(asset_stanzas)
|
||||||
|
|
||||||
|
|
||||||
|
# First element of the returned tuple is the sidechain stanzas
|
||||||
|
# second element is the bootstrap stanzas
|
||||||
|
def generate_sidechain_stanza(
|
||||||
|
mainchain_ports: Ports,
|
||||||
|
main_account: dict,
|
||||||
|
federators: List[Keypair],
|
||||||
|
signing_key: str,
|
||||||
|
mainchain_cfg_file: str,
|
||||||
|
xchain_assets: Optional[Dict[str,
|
||||||
|
XChainAsset]] = None) -> Tuple[str, str]:
|
||||||
|
mainchain_ip = "127.0.0.1"
|
||||||
|
|
||||||
|
federators_stanza = """
|
||||||
|
# federator signing public keys
|
||||||
|
[sidechain_federators]
|
||||||
|
"""
|
||||||
|
federators_secrets_stanza = """
|
||||||
|
# federator signing secret keys (for standalone-mode testing only; Normally won't be in a config file)
|
||||||
|
[sidechain_federators_secrets]
|
||||||
|
"""
|
||||||
|
bootstrap_federators_stanza = """
|
||||||
|
# first value is federator signing public key, second is the signing pk account
|
||||||
|
[sidechain_federators]
|
||||||
|
"""
|
||||||
|
|
||||||
|
assets_stanzas = generate_asset_stanzas(xchain_assets)
|
||||||
|
|
||||||
|
for fed in federators:
|
||||||
|
federators_stanza += f'{fed.public_key}\n'
|
||||||
|
federators_secrets_stanza += f'{fed.secret_key}\n'
|
||||||
|
bootstrap_federators_stanza += f'{fed.public_key} {fed.account_id}\n'
|
||||||
|
|
||||||
|
sidechain_stanzas = f"""
|
||||||
|
[sidechain]
|
||||||
|
signing_key={signing_key}
|
||||||
|
mainchain_account={main_account["account_id"]}
|
||||||
|
mainchain_ip={mainchain_ip}
|
||||||
|
mainchain_port_ws={mainchain_ports.ws_public_port}
|
||||||
|
# mainchain config file is: {mainchain_cfg_file}
|
||||||
|
|
||||||
|
{assets_stanzas}
|
||||||
|
|
||||||
|
{federators_stanza}
|
||||||
|
|
||||||
|
{federators_secrets_stanza}
|
||||||
|
"""
|
||||||
|
bootstrap_stanzas = f"""
|
||||||
|
[sidechain]
|
||||||
|
mainchain_secret={main_account["master_seed"]}
|
||||||
|
|
||||||
|
{bootstrap_federators_stanza}
|
||||||
|
"""
|
||||||
|
return (sidechain_stanzas, bootstrap_stanzas)
|
||||||
|
|
||||||
|
|
||||||
|
# cfg_type will typically be either 'dog' or 'test', but can be any string. It is only used
|
||||||
|
# to create the data directories.
|
||||||
|
def generate_cfg_dir(*,
|
||||||
|
ports: Ports,
|
||||||
|
with_shards: bool,
|
||||||
|
main_net: bool,
|
||||||
|
cfg_type: str,
|
||||||
|
sidechain_stanza: str,
|
||||||
|
sidechain_bootstrap_stanza: str,
|
||||||
|
validation_seed: Optional[str] = None,
|
||||||
|
validators: Optional[List[str]] = None,
|
||||||
|
fixed_ips: Optional[List[Ports]] = None,
|
||||||
|
data_dir: str,
|
||||||
|
full_history: bool = False,
|
||||||
|
with_hooks: bool = False) -> str:
|
||||||
|
ips_stanza = ''
|
||||||
|
this_ip = '127.0.0.1'
|
||||||
|
if fixed_ips:
|
||||||
|
ips_stanza = '# Fixed ips for a testnet.\n'
|
||||||
|
ips_stanza += '[ips_fixed]\n'
|
||||||
|
for i, p in enumerate(fixed_ips):
|
||||||
|
if p.peer_port == ports.peer_port:
|
||||||
|
continue
|
||||||
|
# rippled limits the number of connects per ip. So use the other loopback devices
|
||||||
|
ips_stanza += f'127.0.0.{i+1} {p.peer_port}\n'
|
||||||
|
else:
|
||||||
|
ips_stanza = '# Where to find some other servers speaking the Ripple protocol.\n'
|
||||||
|
ips_stanza += '[ips]\n'
|
||||||
|
if main_net:
|
||||||
|
ips_stanza += 'r.ripple.com 51235\n'
|
||||||
|
else:
|
||||||
|
ips_stanza += 'r.altnet.rippletest.net 51235\n'
|
||||||
|
disable_shards = '' if with_shards else '# '
|
||||||
|
disable_delete = '#' if full_history else ''
|
||||||
|
history_line = 'full' if full_history else '256'
|
||||||
|
earliest_seq_line = ''
|
||||||
|
if sidechain_stanza:
|
||||||
|
earliest_seq_line = 'earliest_seq=1'
|
||||||
|
hooks_line = 'Hooks' if with_hooks else ''
|
||||||
|
validation_seed_stanza = ''
|
||||||
|
if validation_seed:
|
||||||
|
validation_seed_stanza = f'''
|
||||||
|
[validation_seed]
|
||||||
|
{validation_seed}
|
||||||
|
'''
|
||||||
|
node_size = 'medium'
|
||||||
|
shard_str = 'shards' if with_shards else 'no_shards'
|
||||||
|
net_str = 'main' if main_net else 'test'
|
||||||
|
if not fixed_ips:
|
||||||
|
sub_dir = data_dir + f'/{net_str}.{shard_str}.{cfg_type}'
|
||||||
|
if sidechain_stanza:
|
||||||
|
sub_dir += '.sidechain'
|
||||||
|
else:
|
||||||
|
sub_dir = data_dir + f'/{cfg_type}'
|
||||||
|
db_path = sub_dir + '/db'
|
||||||
|
debug_logfile = sub_dir + '/debug.log'
|
||||||
|
shard_db_path = sub_dir + '/shards'
|
||||||
|
node_db_path = db_path + '/nudb'
|
||||||
|
|
||||||
|
cfg_str = f"""
|
||||||
|
[server]
|
||||||
|
port_rpc_admin_local
|
||||||
|
port_peer
|
||||||
|
port_ws_admin_local
|
||||||
|
port_ws_public
|
||||||
|
#ssl_key = /etc/ssl/private/server.key
|
||||||
|
#ssl_cert = /etc/ssl/certs/server.crt
|
||||||
|
|
||||||
|
[port_rpc_admin_local]
|
||||||
|
port = {ports.http_admin_port}
|
||||||
|
ip = {this_ip}
|
||||||
|
admin = {this_ip}
|
||||||
|
protocol = http
|
||||||
|
|
||||||
|
[port_peer]
|
||||||
|
port = {ports.peer_port}
|
||||||
|
ip = 0.0.0.0
|
||||||
|
protocol = peer
|
||||||
|
|
||||||
|
[port_ws_admin_local]
|
||||||
|
port = {ports.ws_admin_port}
|
||||||
|
ip = {this_ip}
|
||||||
|
admin = {this_ip}
|
||||||
|
protocol = ws
|
||||||
|
|
||||||
|
[port_ws_public]
|
||||||
|
port = {ports.ws_public_port}
|
||||||
|
ip = {this_ip}
|
||||||
|
protocol = ws
|
||||||
|
# protocol = wss
|
||||||
|
|
||||||
|
[node_size]
|
||||||
|
{node_size}
|
||||||
|
|
||||||
|
[ledger_history]
|
||||||
|
{history_line}
|
||||||
|
|
||||||
|
[node_db]
|
||||||
|
type=NuDB
|
||||||
|
path={node_db_path}
|
||||||
|
open_files=2000
|
||||||
|
filter_bits=12
|
||||||
|
cache_mb=256
|
||||||
|
file_size_mb=8
|
||||||
|
file_size_mult=2
|
||||||
|
{earliest_seq_line}
|
||||||
|
{disable_delete}online_delete=256
|
||||||
|
{disable_delete}advisory_delete=0
|
||||||
|
|
||||||
|
[database_path]
|
||||||
|
{db_path}
|
||||||
|
|
||||||
|
# This needs to be an absolute directory reference, not a relative one.
|
||||||
|
# Modify this value as required.
|
||||||
|
[debug_logfile]
|
||||||
|
{debug_logfile}
|
||||||
|
|
||||||
|
[sntp_servers]
|
||||||
|
time.windows.com
|
||||||
|
time.apple.com
|
||||||
|
time.nist.gov
|
||||||
|
pool.ntp.org
|
||||||
|
|
||||||
|
{ips_stanza}
|
||||||
|
|
||||||
|
[validators_file]
|
||||||
|
validators.txt
|
||||||
|
|
||||||
|
[rpc_startup]
|
||||||
|
{{ "command": "log_level", "severity": "fatal" }}
|
||||||
|
{{ "command": "log_level", "partition": "SidechainFederator", "severity": "trace" }}
|
||||||
|
|
||||||
|
[ssl_verify]
|
||||||
|
1
|
||||||
|
|
||||||
|
{validation_seed_stanza}
|
||||||
|
|
||||||
|
{disable_shards}[shard_db]
|
||||||
|
{disable_shards}type=NuDB
|
||||||
|
{disable_shards}path={shard_db_path}
|
||||||
|
{disable_shards}max_historical_shards=6
|
||||||
|
|
||||||
|
{sidechain_stanza}
|
||||||
|
|
||||||
|
[features]
|
||||||
|
{hooks_line}
|
||||||
|
PayChan
|
||||||
|
Flow
|
||||||
|
FlowCross
|
||||||
|
TickSize
|
||||||
|
fix1368
|
||||||
|
Escrow
|
||||||
|
fix1373
|
||||||
|
EnforceInvariants
|
||||||
|
SortedDirectories
|
||||||
|
fix1201
|
||||||
|
fix1512
|
||||||
|
fix1513
|
||||||
|
fix1523
|
||||||
|
fix1528
|
||||||
|
DepositAuth
|
||||||
|
Checks
|
||||||
|
fix1571
|
||||||
|
fix1543
|
||||||
|
fix1623
|
||||||
|
DepositPreauth
|
||||||
|
fix1515
|
||||||
|
fix1578
|
||||||
|
MultiSignReserve
|
||||||
|
fixTakerDryOfferRemoval
|
||||||
|
fixMasterKeyAsRegularKey
|
||||||
|
fixCheckThreading
|
||||||
|
fixPayChanRecipientOwnerDir
|
||||||
|
DeletableAccounts
|
||||||
|
fixQualityUpperBound
|
||||||
|
RequireFullyCanonicalSig
|
||||||
|
fix1781
|
||||||
|
HardenedValidations
|
||||||
|
fixAmendmentMajorityCalc
|
||||||
|
NegativeUNL
|
||||||
|
TicketBatch
|
||||||
|
FlowSortStrands
|
||||||
|
fixSTAmountCanonicalize
|
||||||
|
fixRmSmallIncreasedQOffers
|
||||||
|
CheckCashMakesTrustLine
|
||||||
|
"""
|
||||||
|
|
||||||
|
validators_str = ''
|
||||||
|
for p in [sub_dir, db_path, shard_db_path]:
|
||||||
|
Path(p).mkdir(parents=True, exist_ok=True)
|
||||||
|
# Add the validators.txt file
|
||||||
|
if validators:
|
||||||
|
validators_str = '[validators]\n'
|
||||||
|
for k in validators:
|
||||||
|
validators_str += f'{k}\n'
|
||||||
|
else:
|
||||||
|
validators_str = mainnet_validators if main_net else altnet_validators
|
||||||
|
with open(sub_dir + "/validators.txt", "w") as f:
|
||||||
|
f.write(validators_str)
|
||||||
|
|
||||||
|
# add the rippled.cfg file
|
||||||
|
with open(sub_dir + "/rippled.cfg", "w") as f:
|
||||||
|
f.write(cfg_str)
|
||||||
|
|
||||||
|
if sidechain_bootstrap_stanza:
|
||||||
|
# add the bootstrap file
|
||||||
|
with open(sub_dir + "/sidechain_bootstrap.cfg", "w") as f:
|
||||||
|
f.write(sidechain_bootstrap_stanza)
|
||||||
|
|
||||||
|
return sub_dir + "/rippled.cfg"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_multinode_net(out_dir: str,
|
||||||
|
mainnet: Network,
|
||||||
|
sidenet: SidechainNetwork,
|
||||||
|
xchain_assets: Optional[Dict[str,
|
||||||
|
XChainAsset]] = None):
|
||||||
|
mainnet_cfgs = []
|
||||||
|
for i in range(len(mainnet.ports)):
|
||||||
|
validator_kp = mainnet.validator_keypairs[i]
|
||||||
|
ports = mainnet.ports[i]
|
||||||
|
mainchain_cfg_file = generate_cfg_dir(
|
||||||
|
ports=ports,
|
||||||
|
with_shards=False,
|
||||||
|
main_net=True,
|
||||||
|
cfg_type=f'mainchain_{i}',
|
||||||
|
sidechain_stanza='',
|
||||||
|
sidechain_bootstrap_stanza='',
|
||||||
|
validation_seed=validator_kp.secret_key,
|
||||||
|
data_dir=out_dir)
|
||||||
|
mainnet_cfgs.append(mainchain_cfg_file)
|
||||||
|
|
||||||
|
for i in range(len(sidenet.ports)):
|
||||||
|
validator_kp = sidenet.validator_keypairs[i]
|
||||||
|
ports = sidenet.ports[i]
|
||||||
|
|
||||||
|
mainnet_i = i % len(mainnet.ports)
|
||||||
|
sidechain_stanza, sidechain_bootstrap_stanza = generate_sidechain_stanza(
|
||||||
|
mainnet.ports[mainnet_i], sidenet.main_account,
|
||||||
|
sidenet.federator_keypairs,
|
||||||
|
sidenet.federator_keypairs[i].secret_key, mainnet_cfgs[mainnet_i],
|
||||||
|
xchain_assets)
|
||||||
|
|
||||||
|
generate_cfg_dir(
|
||||||
|
ports=ports,
|
||||||
|
with_shards=False,
|
||||||
|
main_net=True,
|
||||||
|
cfg_type=f'sidechain_{i}',
|
||||||
|
sidechain_stanza=sidechain_stanza,
|
||||||
|
sidechain_bootstrap_stanza=sidechain_bootstrap_stanza,
|
||||||
|
validation_seed=validator_kp.secret_key,
|
||||||
|
validators=[kp.public_key for kp in sidenet.validator_keypairs],
|
||||||
|
fixed_ips=sidenet.ports,
|
||||||
|
data_dir=out_dir,
|
||||||
|
full_history=True,
|
||||||
|
with_hooks=False)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=('Create config files for testing sidechains'))
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--exe',
|
||||||
|
'-e',
|
||||||
|
help=('path to rippled executable'),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--usd',
|
||||||
|
'-u',
|
||||||
|
action='store_true',
|
||||||
|
help=('include a USD/root IOU asset for cross chain transfers'),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--cfgs_dir',
|
||||||
|
'-c',
|
||||||
|
help=
|
||||||
|
('path to configuration file dir (where the output config files will be located)'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser.parse_known_args()[0]
|
||||||
|
|
||||||
|
|
||||||
|
class Params:
|
||||||
|
def __init__(self):
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
self.exe = None
|
||||||
|
if 'RIPPLED_MAINCHAIN_EXE' in os.environ:
|
||||||
|
self.exe = os.environ['RIPPLED_MAINCHAIN_EXE']
|
||||||
|
if args.exe:
|
||||||
|
self.exe = args.exe
|
||||||
|
|
||||||
|
self.configs_dir = None
|
||||||
|
if 'RIPPLED_SIDECHAIN_CFG_DIR' in os.environ:
|
||||||
|
self.configs_dir = os.environ['RIPPLED_SIDECHAIN_CFG_DIR']
|
||||||
|
if args.cfgs_dir:
|
||||||
|
self.configs_dir = args.cfgs_dir
|
||||||
|
|
||||||
|
self.usd = False
|
||||||
|
if args.usd:
|
||||||
|
self.usd = args.usd
|
||||||
|
|
||||||
|
def check_error(self) -> str:
|
||||||
|
'''
|
||||||
|
Check for errors. Return `None` if no errors,
|
||||||
|
otherwise return a string describing the error
|
||||||
|
'''
|
||||||
|
if not self.exe:
|
||||||
|
return 'Missing exe location. Either set the env variable RIPPLED_MAINCHAIN_EXE or use the --exe_mainchain command line switch'
|
||||||
|
if not self.configs_dir:
|
||||||
|
return 'Missing configs directory location. Either set the env variable RIPPLED_SIDECHAIN_CFG_DIR or use the --cfgs_dir command line switch'
|
||||||
|
|
||||||
|
|
||||||
|
def main(params: Params,
|
||||||
|
xchain_assets: Optional[Dict[str, XChainAsset]] = None):
|
||||||
|
|
||||||
|
if err_str := params.check_error():
|
||||||
|
eprint(err_str)
|
||||||
|
sys.exit(1)
|
||||||
|
index = 0
|
||||||
|
nonvalidator_cfg_file_name = generate_cfg_dir(
|
||||||
|
ports=Ports(index),
|
||||||
|
with_shards=False,
|
||||||
|
main_net=True,
|
||||||
|
cfg_type='non_validator',
|
||||||
|
sidechain_stanza='',
|
||||||
|
sidechain_bootstrap_stanza='',
|
||||||
|
validation_seed=None,
|
||||||
|
data_dir=params.configs_dir)
|
||||||
|
index = index + 1
|
||||||
|
|
||||||
|
nonvalidator_config = ConfigFile(file_name=nonvalidator_cfg_file_name)
|
||||||
|
with single_client_app(exe=params.exe,
|
||||||
|
config=nonvalidator_config,
|
||||||
|
standalone=True) as rip:
|
||||||
|
mainnet = Network(num_nodes=1,
|
||||||
|
num_validators=1,
|
||||||
|
start_cfg_index=index,
|
||||||
|
rip=rip)
|
||||||
|
sidenet = SidechainNetwork(num_nodes=5,
|
||||||
|
num_federators=5,
|
||||||
|
num_validators=5,
|
||||||
|
start_cfg_index=index + 1,
|
||||||
|
rip=rip)
|
||||||
|
generate_multinode_net(
|
||||||
|
out_dir=f'{params.configs_dir}/sidechain_testnet',
|
||||||
|
mainnet=mainnet,
|
||||||
|
sidenet=sidenet,
|
||||||
|
xchain_assets=xchain_assets)
|
||||||
|
index = index + 2
|
||||||
|
|
||||||
|
(Path(params.configs_dir) / 'logs').mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for with_shards in [True, False]:
|
||||||
|
for is_main_net in [True, False]:
|
||||||
|
for cfg_type in ['dog', 'test', 'one', 'two']:
|
||||||
|
if not is_main_net and cfg_type not in ['dog', 'test']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
mainnet = Network(num_nodes=1,
|
||||||
|
num_validators=1,
|
||||||
|
start_cfg_index=index,
|
||||||
|
rip=rip)
|
||||||
|
mainchain_cfg_file = generate_cfg_dir(
|
||||||
|
data_dir=params.configs_dir,
|
||||||
|
ports=mainnet.ports[0],
|
||||||
|
with_shards=with_shards,
|
||||||
|
main_net=is_main_net,
|
||||||
|
cfg_type=cfg_type,
|
||||||
|
sidechain_stanza='',
|
||||||
|
sidechain_bootstrap_stanza='',
|
||||||
|
validation_seed=mainnet.validator_keypairs[0].
|
||||||
|
secret_key)
|
||||||
|
|
||||||
|
sidenet = SidechainNetwork(num_nodes=1,
|
||||||
|
num_federators=5,
|
||||||
|
num_validators=1,
|
||||||
|
start_cfg_index=index + 1,
|
||||||
|
rip=rip)
|
||||||
|
signing_key = sidenet.federator_keypairs[0].secret_key
|
||||||
|
|
||||||
|
sidechain_stanza, sizechain_bootstrap_stanza = generate_sidechain_stanza(
|
||||||
|
mainnet.ports[0], sidenet.main_account,
|
||||||
|
sidenet.federator_keypairs, signing_key,
|
||||||
|
mainchain_cfg_file, xchain_assets)
|
||||||
|
|
||||||
|
generate_cfg_dir(
|
||||||
|
data_dir=params.configs_dir,
|
||||||
|
ports=sidenet.ports[0],
|
||||||
|
with_shards=with_shards,
|
||||||
|
main_net=is_main_net,
|
||||||
|
cfg_type=cfg_type,
|
||||||
|
sidechain_stanza=sidechain_stanza,
|
||||||
|
sidechain_bootstrap_stanza=sizechain_bootstrap_stanza,
|
||||||
|
validation_seed=sidenet.validator_keypairs[0].
|
||||||
|
secret_key)
|
||||||
|
index = index + 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
params = Params()
|
||||||
|
|
||||||
|
xchain_assets = None
|
||||||
|
if params.usd:
|
||||||
|
xchain_assets = {}
|
||||||
|
xchain_assets['xrp_xrp_sidechain_asset'] = XChainAsset(
|
||||||
|
XRP(0), XRP(0), 1, 1, 200, 200)
|
||||||
|
root_account = Account(account_id="rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh")
|
||||||
|
main_iou_asset = Asset(value=0, currency='USD', issuer=root_account)
|
||||||
|
side_iou_asset = Asset(value=0, currency='USD', issuer=root_account)
|
||||||
|
xchain_assets['iou_iou_sidechain_asset'] = XChainAsset(
|
||||||
|
main_iou_asset, side_iou_asset, 1, 1, 0.02, 0.02)
|
||||||
|
|
||||||
|
main(params, xchain_assets)
|
||||||
1496
bin/sidechain/python/interactive.py
Normal file
1496
bin/sidechain/python/interactive.py
Normal file
File diff suppressed because it is too large
Load Diff
198
bin/sidechain/python/log_analyzer.py
Executable file
198
bin/sidechain/python/log_analyzer.py
Executable file
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from common import eprint
|
||||||
|
from typing import IO, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class LogLine:
|
||||||
|
UNSTRUCTURED_RE = re.compile(r'''(?x)
|
||||||
|
# The x flag enables insignificant whitespace mode (allowing comments)
|
||||||
|
^(?P<timestamp>.*UTC)
|
||||||
|
[\ ]
|
||||||
|
(?P<module>[^:]*):(?P<level>[^\ ]*)
|
||||||
|
[\ ]
|
||||||
|
(?P<msg>.*$)
|
||||||
|
''')
|
||||||
|
|
||||||
|
STRUCTURED_RE = re.compile(r'''(?x)
|
||||||
|
# The x flag enables insignificant whitespace mode (allowing comments)
|
||||||
|
^(?P<timestamp>.*UTC)
|
||||||
|
[\ ]
|
||||||
|
(?P<module>[^:]*):(?P<level>[^\ ]*)
|
||||||
|
[\ ]
|
||||||
|
(?P<msg>[^{]*)
|
||||||
|
[\ ]
|
||||||
|
(?P<json_data>.*$)
|
||||||
|
''')
|
||||||
|
|
||||||
|
def __init__(self, line: str):
|
||||||
|
self.raw_line = line
|
||||||
|
self.json_data = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if line.endswith('}'):
|
||||||
|
m = self.STRUCTURED_RE.match(line)
|
||||||
|
try:
|
||||||
|
self.json_data = json.loads(m.group('json_data'))
|
||||||
|
except:
|
||||||
|
m = self.UNSTRUCTURED_RE.match(line)
|
||||||
|
else:
|
||||||
|
m = self.UNSTRUCTURED_RE.match(line)
|
||||||
|
|
||||||
|
self.timestamp = m.group('timestamp')
|
||||||
|
self.level = m.group('level')
|
||||||
|
self.module = m.group('module')
|
||||||
|
self.msg = m.group('msg')
|
||||||
|
except Exception as e:
|
||||||
|
eprint(f'init exception: {e} line: {line}')
|
||||||
|
|
||||||
|
def to_mixed_json_str(self) -> str:
|
||||||
|
'''
|
||||||
|
return a pretty printed string as mixed json
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
r = f'{self.timestamp} {self.module}:{self.level} {self.msg}'
|
||||||
|
if self.json_data:
|
||||||
|
r += '\n' + json.dumps(self.json_data, indent=1)
|
||||||
|
return r
|
||||||
|
except:
|
||||||
|
eprint(f'Using raw line: {self.raw_line}')
|
||||||
|
return self.raw_line
|
||||||
|
|
||||||
|
def to_pure_json(self) -> dict:
|
||||||
|
'''
|
||||||
|
return a json dict
|
||||||
|
'''
|
||||||
|
dict = {}
|
||||||
|
dict['t'] = self.timestamp
|
||||||
|
dict['m'] = self.module
|
||||||
|
dict['l'] = self.level
|
||||||
|
dict['msg'] = self.msg
|
||||||
|
if self.json_data:
|
||||||
|
dict['data'] = self.json_data
|
||||||
|
return dict
|
||||||
|
|
||||||
|
def to_pure_json_str(self, f_id: Optional[str] = None) -> str:
|
||||||
|
'''
|
||||||
|
return a pretty printed string as pure json
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
dict = self.to_pure_json(f_id)
|
||||||
|
return json.dumps(dict, indent=1)
|
||||||
|
except:
|
||||||
|
return self.raw_line
|
||||||
|
|
||||||
|
|
||||||
|
def convert_log(in_file_name: str,
|
||||||
|
out: str,
|
||||||
|
*,
|
||||||
|
as_list=False,
|
||||||
|
pure_json=False,
|
||||||
|
module: Optional[str] = 'SidechainFederator') -> list:
|
||||||
|
result = []
|
||||||
|
try:
|
||||||
|
prev_lines = None
|
||||||
|
with open(in_file_name) as input:
|
||||||
|
for l in input:
|
||||||
|
l = l.strip()
|
||||||
|
if not l:
|
||||||
|
continue
|
||||||
|
if LogLine.UNSTRUCTURED_RE.match(l):
|
||||||
|
if prev_lines:
|
||||||
|
log_line = LogLine(prev_lines)
|
||||||
|
if not module or log_line.module == module:
|
||||||
|
if as_list:
|
||||||
|
result.append(log_line.to_pure_json())
|
||||||
|
else:
|
||||||
|
if pure_json:
|
||||||
|
print(log_line.to_pure_json_str(),
|
||||||
|
file=out)
|
||||||
|
else:
|
||||||
|
print(log_line.to_mixed_json_str(),
|
||||||
|
file=out)
|
||||||
|
prev_lines = l
|
||||||
|
else:
|
||||||
|
if not prev_lines:
|
||||||
|
eprint(f'Error: Expected prev_lines. Cur line: {l}')
|
||||||
|
assert prev_lines
|
||||||
|
prev_lines += f' {l}'
|
||||||
|
if prev_lines:
|
||||||
|
log_line = LogLine(prev_lines)
|
||||||
|
if not module or log_line.module == module:
|
||||||
|
if as_list:
|
||||||
|
result.append(log_line.to_pure_json())
|
||||||
|
else:
|
||||||
|
if pure_json:
|
||||||
|
print(log_line.to_pure_json_str(f_id),
|
||||||
|
file=out,
|
||||||
|
flush=True)
|
||||||
|
else:
|
||||||
|
print(log_line.to_mixed_json_str(),
|
||||||
|
file=out,
|
||||||
|
flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
eprint(f'Excption: {e}')
|
||||||
|
raise e
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def convert_all(in_dir_name: str, out: IO, *, pure_json=False):
|
||||||
|
'''
|
||||||
|
Convert all the "debug.log" log files in one directory level below the in_dir_name into a single json file.
|
||||||
|
There will be a field called 'f' for the director name that the origional log file came from.
|
||||||
|
This is useful when analyzing networks that run on the local machine.
|
||||||
|
'''
|
||||||
|
if not os.path.isdir(in_dir_name):
|
||||||
|
print(f'Error: {in_dir_name} is not a directory')
|
||||||
|
files = []
|
||||||
|
f_ids = []
|
||||||
|
for subdir in os.listdir(in_dir_name):
|
||||||
|
file = f'{in_dir_name}/{subdir}/debug.log'
|
||||||
|
if not os.path.isfile(file):
|
||||||
|
continue
|
||||||
|
files.append(file)
|
||||||
|
f_ids.append(subdir)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
for f, f_id in zip(files, f_ids):
|
||||||
|
l = convert_log(f, out, as_list=True, pure_json=pure_json, module=None)
|
||||||
|
result[f_id] = l
|
||||||
|
print(json.dumps(result, indent=1), file=out, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=('python script to convert log files to json'))
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--input',
|
||||||
|
'-i',
|
||||||
|
help=('input log file or sidechain config directory structure'),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--output',
|
||||||
|
'-o',
|
||||||
|
help=('output log file'),
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser.parse_known_args()[0]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
args = parse_args()
|
||||||
|
with open(args.output, "w") as out:
|
||||||
|
if os.path.isdir(args.input):
|
||||||
|
convert_all(args.input, out, pure_json=True)
|
||||||
|
else:
|
||||||
|
convert_log(args.input, out, pure_json=True)
|
||||||
|
except Exception as e:
|
||||||
|
eprint(f'Excption: {e}')
|
||||||
|
raise e
|
||||||
285
bin/sidechain/python/log_report.py
Executable file
285
bin/sidechain/python/log_report.py
Executable file
@@ -0,0 +1,285 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from collections import defaultdict
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
import string
|
||||||
|
import sys
|
||||||
|
from typing import Dict, Set
|
||||||
|
|
||||||
|
from common import eprint
|
||||||
|
import log_analyzer
|
||||||
|
|
||||||
|
|
||||||
|
def _has_256bit_hex_field_other(data, result: Set[str]):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
_has_256bit_hex_field_overloads = defaultdict(
|
||||||
|
lambda: _has_256bit_hex_field_other)
|
||||||
|
|
||||||
|
|
||||||
|
def _has_256bit_hex_field_str(data: str, result: Set[str]):
|
||||||
|
if len(data) != 64:
|
||||||
|
return
|
||||||
|
for c in data:
|
||||||
|
o = ord(c.upper())
|
||||||
|
if ord('A') <= o <= ord('F'):
|
||||||
|
continue
|
||||||
|
if ord('0') <= o <= ord('9'):
|
||||||
|
continue
|
||||||
|
return
|
||||||
|
result.add(data)
|
||||||
|
|
||||||
|
|
||||||
|
_has_256bit_hex_field_overloads[str] = _has_256bit_hex_field_str
|
||||||
|
|
||||||
|
|
||||||
|
def _has_256bit_hex_field_dict(data: dict, result: Set[str]):
|
||||||
|
for k, v in data.items():
|
||||||
|
if k in [
|
||||||
|
"meta", "index", "LedgerIndex", "ledger_index", "ledger_hash",
|
||||||
|
"SigningPubKey", "suppression"
|
||||||
|
]:
|
||||||
|
continue
|
||||||
|
_has_256bit_hex_field_overloads[type(v)](v, result)
|
||||||
|
|
||||||
|
|
||||||
|
_has_256bit_hex_field_overloads[dict] = _has_256bit_hex_field_dict
|
||||||
|
|
||||||
|
|
||||||
|
def _has_256bit_hex_field_list(data: list, result: Set[str]):
|
||||||
|
for v in data:
|
||||||
|
_has_256bit_hex_field_overloads[type(v)](v, result)
|
||||||
|
|
||||||
|
|
||||||
|
_has_256bit_hex_field_overloads[list] = _has_256bit_hex_field_list
|
||||||
|
|
||||||
|
|
||||||
|
def has_256bit_hex_field(data: dict) -> Set[str]:
|
||||||
|
'''
|
||||||
|
Find all the fields that are strings 64 chars long with only hex digits
|
||||||
|
This is useful when grouping transactions by hex
|
||||||
|
'''
|
||||||
|
result = set()
|
||||||
|
_has_256bit_hex_field_dict(data, result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def group_by_txn(data: dict) -> dict:
|
||||||
|
'''
|
||||||
|
return a dictionary where the key is the transaction hash, the value is another dictionary.
|
||||||
|
The second dictionary the key is the server id, and the values are a list of log items
|
||||||
|
'''
|
||||||
|
def _make_default():
|
||||||
|
return defaultdict(lambda: list())
|
||||||
|
|
||||||
|
result = defaultdict(_make_default)
|
||||||
|
for server_id, log_list in data.items():
|
||||||
|
for log_item in log_list:
|
||||||
|
if txn_hashes := has_256bit_hex_field(log_item):
|
||||||
|
for h in txn_hashes:
|
||||||
|
result[h][server_id].append(log_item)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _rekey_dict_by_txn_date(hash_to_timestamp: dict,
|
||||||
|
grouped_by_txn: dict) -> dict:
|
||||||
|
'''
|
||||||
|
hash_to_timestamp is a dictionary with a key of the txn hash and a value of the timestamp.
|
||||||
|
grouped_by_txn is a dictionary with a key of the txn and an unspecified value.
|
||||||
|
the keys in hash_to_timestamp are a superset of the keys in grouped_by_txn
|
||||||
|
This function returns a new grouped_by_txn dictionary with the transactions sorted by date.
|
||||||
|
'''
|
||||||
|
known_txns = [
|
||||||
|
k for k, v in sorted(hash_to_timestamp.items(), key=lambda x: x[1])
|
||||||
|
]
|
||||||
|
result = {}
|
||||||
|
for k, v in grouped_by_txn.items():
|
||||||
|
if k not in known_txns:
|
||||||
|
result[k] = v
|
||||||
|
for h in known_txns:
|
||||||
|
result[h] = grouped_by_txn[h]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _to_timestamp(str_time: str) -> datetime.datetime:
|
||||||
|
return datetime.datetime.strptime(
|
||||||
|
str_time.split('.')[0], "%Y-%b-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
|
class Report:
|
||||||
|
def __init__(self, in_dir, out_dir):
|
||||||
|
self.in_dir = in_dir
|
||||||
|
self.out_dir = out_dir
|
||||||
|
|
||||||
|
self.combined_logs_file_name = f'{self.out_dir}/combined_logs.json'
|
||||||
|
self.grouped_by_txn_file_name = f'{self.out_dir}/grouped_by_txn.json'
|
||||||
|
self.counts_by_txn_and_server_file_name = f'{self.out_dir}/counts_by_txn_and_server.org'
|
||||||
|
self.data = None # combined logs
|
||||||
|
|
||||||
|
# grouped_by_txn is a dictionary where the key is the server id. mainchain servers
|
||||||
|
# have a key of `mainchain_#` and sidechain servers have a key of
|
||||||
|
# `sidechain_#`, where `#` is a number.
|
||||||
|
self.grouped_by_txn = None
|
||||||
|
|
||||||
|
if not os.path.isdir(in_dir):
|
||||||
|
eprint(f'The input {self.in_dir} must be an existing directory')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if os.path.exists(self.out_dir):
|
||||||
|
if not os.path.isdir(self.out_dir):
|
||||||
|
eprint(
|
||||||
|
f'The output: {self.out_dir} exists and is not a directory'
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
os.makedirs(self.out_dir)
|
||||||
|
|
||||||
|
self.combine_logs()
|
||||||
|
with open(self.combined_logs_file_name) as f:
|
||||||
|
self.data = json.load(f)
|
||||||
|
self.grouped_by_txn = group_by_txn(self.data)
|
||||||
|
|
||||||
|
# counts_by_txn_and_server is a dictionary where the key is the txn_hash
|
||||||
|
# and the value is a pandas df with a row for every server and a column for every message
|
||||||
|
# the value is a count of how many times that message appears for that server.
|
||||||
|
counts_by_txn_and_server = {}
|
||||||
|
# dict where the key is a transaction hash and the value is the transaction
|
||||||
|
hash_to_txn = {}
|
||||||
|
# dict where the key is a transaction hash and the value is earliest timestamp in a log file
|
||||||
|
hash_to_timestamp = {}
|
||||||
|
for txn_hash, server_dict in self.grouped_by_txn.items():
|
||||||
|
message_set = set()
|
||||||
|
# message list is ordered by when it appears in the log
|
||||||
|
message_list = []
|
||||||
|
for server_id, messages in server_dict.items():
|
||||||
|
for m in messages:
|
||||||
|
try:
|
||||||
|
d = m['data']
|
||||||
|
if 'msg' in d and 'transaction' in d['msg']:
|
||||||
|
t = d['msg']['transaction']
|
||||||
|
elif 'tx_json' in d:
|
||||||
|
t = d['tx_json']
|
||||||
|
if t['hash'] == txn_hash:
|
||||||
|
hash_to_txn[txn_hash] = t
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
msg = m['msg']
|
||||||
|
t = _to_timestamp(m['t'])
|
||||||
|
if txn_hash not in hash_to_timestamp:
|
||||||
|
hash_to_timestamp[txn_hash] = t
|
||||||
|
elif hash_to_timestamp[txn_hash] > t:
|
||||||
|
hash_to_timestamp[txn_hash] = t
|
||||||
|
if msg not in message_set:
|
||||||
|
message_set.add(msg)
|
||||||
|
message_list.append(msg)
|
||||||
|
df = pd.DataFrame(0,
|
||||||
|
index=server_dict.keys(),
|
||||||
|
columns=message_list)
|
||||||
|
for server_id, messages in server_dict.items():
|
||||||
|
for m in messages:
|
||||||
|
df[m['msg']][server_id] += 1
|
||||||
|
counts_by_txn_and_server[txn_hash] = df
|
||||||
|
|
||||||
|
# sort the transactions by timestamp, but the txns with unknown timestamp at the beginning
|
||||||
|
self.grouped_by_txn = _rekey_dict_by_txn_date(hash_to_timestamp,
|
||||||
|
self.grouped_by_txn)
|
||||||
|
counts_by_txn_and_server = _rekey_dict_by_txn_date(
|
||||||
|
hash_to_timestamp, counts_by_txn_and_server)
|
||||||
|
|
||||||
|
with open(self.grouped_by_txn_file_name, 'w') as out:
|
||||||
|
print(json.dumps(self.grouped_by_txn, indent=1), file=out)
|
||||||
|
|
||||||
|
with open(self.counts_by_txn_and_server_file_name, 'w') as out:
|
||||||
|
for txn_hash, df in counts_by_txn_and_server.items():
|
||||||
|
print(f'\n\n* Txn: {txn_hash}', file=out)
|
||||||
|
if txn_hash in hash_to_txn:
|
||||||
|
print(json.dumps(hash_to_txn[txn_hash], indent=1),
|
||||||
|
file=out)
|
||||||
|
rename_dict = {}
|
||||||
|
for column, renamed_column in zip(df.columns.array,
|
||||||
|
string.ascii_uppercase):
|
||||||
|
print(f'{renamed_column} = {column}', file=out)
|
||||||
|
rename_dict[column] = renamed_column
|
||||||
|
df.rename(columns=rename_dict, inplace=True)
|
||||||
|
print(f'\n{df}', file=out)
|
||||||
|
|
||||||
|
def combine_logs(self):
|
||||||
|
try:
|
||||||
|
with open(self.combined_logs_file_name, "w") as out:
|
||||||
|
log_analyzer.convert_all(args.input, out, pure_json=True)
|
||||||
|
except Exception as e:
|
||||||
|
eprint(f'Excption: {e}')
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def main(input_dir_name: str, output_dir_name: str):
|
||||||
|
r = Report(input_dir_name, output_dir_name)
|
||||||
|
|
||||||
|
# Values are a list of log lines formatted as json. There are five fields:
|
||||||
|
# `t` is the timestamp.
|
||||||
|
# `m` is the module.
|
||||||
|
# `l` is the log level.
|
||||||
|
# `msg` is the message.
|
||||||
|
# `data` is the data.
|
||||||
|
# For example:
|
||||||
|
#
|
||||||
|
# {
|
||||||
|
# "t": "2021-Oct-08 21:33:41.731371562 UTC",
|
||||||
|
# "m": "SidechainFederator",
|
||||||
|
# "l": "TRC",
|
||||||
|
# "msg": "no last xchain txn with result",
|
||||||
|
# "data": {
|
||||||
|
# "needsOtherChainLastXChainTxn": true,
|
||||||
|
# "isMainchain": false,
|
||||||
|
# "jlogId": 121
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
|
||||||
|
|
||||||
|
# Lifecycle of a transaction
|
||||||
|
# For each federator record:
|
||||||
|
# Transaction detected: amount, seq, destination, chain, hash
|
||||||
|
# Signature received: hash, seq
|
||||||
|
# Signature sent: hash, seq, federator dst
|
||||||
|
# Transaction submitted
|
||||||
|
# Result received, and detect if error
|
||||||
|
# Detect any field that doesn't match
|
||||||
|
|
||||||
|
# Lifecycle of initialization
|
||||||
|
|
||||||
|
# Chain listener messages
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser(description=(
|
||||||
|
'python script to generate a log report from a sidechain config directory structure containing the logs'
|
||||||
|
))
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--input',
|
||||||
|
'-i',
|
||||||
|
help=('directory with sidechain config directory structure'),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--output',
|
||||||
|
'-o',
|
||||||
|
help=('output directory for report files'),
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser.parse_known_args()[0]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
args = parse_args()
|
||||||
|
main(args.input, args.output)
|
||||||
|
except Exception as e:
|
||||||
|
eprint(f'Excption: {e}')
|
||||||
|
raise e
|
||||||
14
bin/sidechain/python/requirements.txt
Normal file
14
bin/sidechain/python/requirements.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
attrs==21.2.0
|
||||||
|
iniconfig==1.1.1
|
||||||
|
numpy==1.21.2
|
||||||
|
packaging==21.0
|
||||||
|
pandas==1.3.3
|
||||||
|
pluggy==1.0.0
|
||||||
|
py==1.10.0
|
||||||
|
pyparsing==2.4.7
|
||||||
|
pytest==6.2.5
|
||||||
|
python-dateutil==2.8.2
|
||||||
|
pytz==2021.1
|
||||||
|
six==1.16.0
|
||||||
|
toml==0.10.2
|
||||||
|
websockets==8.1
|
||||||
35
bin/sidechain/python/riplrepl.py
Executable file
35
bin/sidechain/python/riplrepl.py
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
'''
|
||||||
|
Script to run an interactive shell to test sidechains.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from common import disable_eprint, eprint
|
||||||
|
import interactive
|
||||||
|
import sidechain
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
params = sidechain.Params()
|
||||||
|
params.interactive = True
|
||||||
|
|
||||||
|
interactive.set_hooks_dir(params.hooks_dir)
|
||||||
|
|
||||||
|
if err_str := params.check_error():
|
||||||
|
eprint(err_str)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if params.verbose:
|
||||||
|
print("eprint enabled")
|
||||||
|
else:
|
||||||
|
disable_eprint()
|
||||||
|
|
||||||
|
if params.standalone:
|
||||||
|
sidechain.standalone_interactive_repl(params)
|
||||||
|
else:
|
||||||
|
sidechain.multinode_interactive_repl(params)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
193
bin/sidechain/python/ripple_client.py
Normal file
193
bin/sidechain/python/ripple_client.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from os.path import expanduser
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from typing import Callable, List, Optional, Union
|
||||||
|
import time
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
from command import Command, ServerInfo, SubscriptionCommand
|
||||||
|
from common import eprint
|
||||||
|
from config_file import ConfigFile
|
||||||
|
|
||||||
|
|
||||||
|
class RippleClient:
|
||||||
|
'''Client to send commands to the rippled server'''
|
||||||
|
def __init__(self,
|
||||||
|
*,
|
||||||
|
config: ConfigFile,
|
||||||
|
exe: str,
|
||||||
|
command_log: Optional[str] = None):
|
||||||
|
self.config = config
|
||||||
|
self.exe = exe
|
||||||
|
self.command_log = command_log
|
||||||
|
section = config.port_ws_admin_local
|
||||||
|
self.websocket_uri = f'{section.protocol}://{section.ip}:{section.port}'
|
||||||
|
self.subscription_websockets = []
|
||||||
|
self.tasks = []
|
||||||
|
self.pid = None
|
||||||
|
if command_log:
|
||||||
|
with open(self.command_log, 'w') as f:
|
||||||
|
f.write(f'# Start \n')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config_file_name(self):
|
||||||
|
return self.config.get_file_name()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
try:
|
||||||
|
group = asyncio.gather(*self.tasks, return_exceptions=True)
|
||||||
|
group.cancel()
|
||||||
|
asyncio.get_event_loop().run_until_complete(group)
|
||||||
|
for ws in self.subscription_websockets:
|
||||||
|
asyncio.get_event_loop().run_until_complete(ws.close())
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_pid(self, pid: int):
|
||||||
|
self.pid = pid
|
||||||
|
|
||||||
|
def get_pid(self) -> Optional[int]:
|
||||||
|
return self.pid
|
||||||
|
|
||||||
|
def get_config(self) -> ConfigFile:
|
||||||
|
return self.config
|
||||||
|
|
||||||
|
# Get a dict of the server_state, validated_ledger_seq, and complete_ledgers
|
||||||
|
def get_brief_server_info(self) -> dict:
|
||||||
|
ret = {
|
||||||
|
'server_state': 'NA',
|
||||||
|
'ledger_seq': 'NA',
|
||||||
|
'complete_ledgers': 'NA'
|
||||||
|
}
|
||||||
|
if not self.pid or self.pid == -1:
|
||||||
|
return ret
|
||||||
|
r = self.send_command(ServerInfo())
|
||||||
|
if 'info' not in r:
|
||||||
|
return ret
|
||||||
|
r = r['info']
|
||||||
|
for f in ['server_state', 'complete_ledgers']:
|
||||||
|
if f in r:
|
||||||
|
ret[f] = r[f]
|
||||||
|
if 'validated_ledger' in r:
|
||||||
|
ret['ledger_seq'] = r['validated_ledger']['seq']
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _write_command_log_command(self, cmd: str, cmd_index: int) -> None:
|
||||||
|
if not self.command_log:
|
||||||
|
return
|
||||||
|
with open(self.command_log, 'a') as f:
|
||||||
|
f.write(f'\n\n# command {cmd_index}\n')
|
||||||
|
f.write(f'{cmd}')
|
||||||
|
|
||||||
|
def _write_command_log_result(self, result: str, cmd_index: int) -> None:
|
||||||
|
if not self.command_log:
|
||||||
|
return
|
||||||
|
with open(self.command_log, 'a') as f:
|
||||||
|
f.write(f'\n\n# result {cmd_index}\n')
|
||||||
|
f.write(f'{result}')
|
||||||
|
|
||||||
|
def _send_command_line_command(self, cmd_id: int, *args) -> dict:
|
||||||
|
'''Send the command to the rippled server using the command line interface'''
|
||||||
|
to_run = [self.exe, '-q', '--conf', self.config_file_name, '--']
|
||||||
|
to_run.extend(args)
|
||||||
|
self._write_command_log_command(to_run, cmd_id)
|
||||||
|
max_retries = 4
|
||||||
|
for retry_count in range(0, max_retries + 1):
|
||||||
|
try:
|
||||||
|
r = subprocess.check_output(to_run)
|
||||||
|
self._write_command_log_result(r, cmd_id)
|
||||||
|
return json.loads(r.decode('utf-8'))['result']
|
||||||
|
except Exception as e:
|
||||||
|
if retry_count == max_retries:
|
||||||
|
raise
|
||||||
|
eprint(
|
||||||
|
f'Got exception: {str(e)}\nretrying..{retry_count+1} of {max_retries}'
|
||||||
|
)
|
||||||
|
time.sleep(1) # give process time to startup
|
||||||
|
|
||||||
|
async def _send_websock_command(
|
||||||
|
self,
|
||||||
|
cmd: Command,
|
||||||
|
conn: Optional[websockets.client.Connect] = None) -> dict:
|
||||||
|
assert self.websocket_uri
|
||||||
|
if conn is None:
|
||||||
|
async with websockets.connect(self.websocket_uri) as ws:
|
||||||
|
return await self._send_websock_command(cmd, ws)
|
||||||
|
|
||||||
|
to_send = json.dumps(cmd.get_websocket_dict())
|
||||||
|
self._write_command_log_command(to_send, cmd.cmd_id)
|
||||||
|
await conn.send(to_send)
|
||||||
|
r = await conn.recv()
|
||||||
|
self._write_command_log_result(r, cmd.cmd_id)
|
||||||
|
j = json.loads(r)
|
||||||
|
if not 'result' in j:
|
||||||
|
eprint(
|
||||||
|
f'Error sending websocket command: {json.dumps(cmd.get_websocket_dict(), indent=1)}'
|
||||||
|
)
|
||||||
|
eprint(f'Result: {json.dumps(j, indent=1)}')
|
||||||
|
raise ValueError('Error sending websocket command')
|
||||||
|
return j['result']
|
||||||
|
|
||||||
|
def send_command(self, cmd: Command) -> dict:
|
||||||
|
'''Send the command to the rippled server'''
|
||||||
|
if self.websocket_uri:
|
||||||
|
return asyncio.get_event_loop().run_until_complete(
|
||||||
|
self._send_websock_command(cmd))
|
||||||
|
return self._send_command_line_command(cmd.cmd_id,
|
||||||
|
*cmd.get_command_line_list())
|
||||||
|
|
||||||
|
# Need async version to close ledgers from async functions
|
||||||
|
async def async_send_command(self, cmd: Command) -> dict:
|
||||||
|
'''Send the command to the rippled server'''
|
||||||
|
if self.websocket_uri:
|
||||||
|
return await self._send_websock_command(cmd)
|
||||||
|
return self._send_command_line_command(cmd.cmd_id,
|
||||||
|
*cmd.get_command_line_list())
|
||||||
|
|
||||||
|
def send_subscribe_command(
|
||||||
|
self,
|
||||||
|
cmd: SubscriptionCommand,
|
||||||
|
callback: Optional[Callable[[dict], None]] = None) -> dict:
|
||||||
|
'''Send the command to the rippled server'''
|
||||||
|
assert self.websocket_uri
|
||||||
|
ws = cmd.websocket
|
||||||
|
if ws is None:
|
||||||
|
# subscribe
|
||||||
|
assert callback
|
||||||
|
ws = asyncio.get_event_loop().run_until_complete(
|
||||||
|
websockets.connect(self.websocket_uri))
|
||||||
|
self.subscription_websockets.append(ws)
|
||||||
|
result = asyncio.get_event_loop().run_until_complete(
|
||||||
|
self._send_websock_command(cmd, ws))
|
||||||
|
if cmd.websocket is not None:
|
||||||
|
# unsubscribed. close the websocket
|
||||||
|
self.subscription_websockets.remove(cmd.websocket)
|
||||||
|
cmd.websocket.close()
|
||||||
|
cmd.websocket = None
|
||||||
|
else:
|
||||||
|
# setup a task to read the websocket
|
||||||
|
cmd.websocket = ws # must be set after the _send_websock_command or will unsubscribe
|
||||||
|
|
||||||
|
async def subscribe_callback(ws: websockets.client.Connect,
|
||||||
|
cb: Callable[[dict], None]):
|
||||||
|
while True:
|
||||||
|
r = await ws.recv()
|
||||||
|
d = json.loads(r)
|
||||||
|
cb(d)
|
||||||
|
|
||||||
|
task = asyncio.get_event_loop().create_task(
|
||||||
|
subscribe_callback(cmd.websocket, callback))
|
||||||
|
self.tasks.append(task)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
'''Stop the server'''
|
||||||
|
return self.send_command(Stop())
|
||||||
|
|
||||||
|
def set_log_level(self, severity: str, *, partition: Optional[str] = None):
|
||||||
|
'''Set the server log level'''
|
||||||
|
return self.send_command(LogLevel(severity, parition=parition))
|
||||||
583
bin/sidechain/python/sidechain.py
Executable file
583
bin/sidechain/python/sidechain.py
Executable file
@@ -0,0 +1,583 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
'''
|
||||||
|
Script to test and debug sidechains.
|
||||||
|
|
||||||
|
The mainchain exe location can be set through the command line or
|
||||||
|
the environment variable RIPPLED_MAINCHAIN_EXE
|
||||||
|
|
||||||
|
The sidechain exe location can be set through the command line or
|
||||||
|
the environment variable RIPPLED_SIDECHAIN_EXE
|
||||||
|
|
||||||
|
The configs_dir (generated with create_config_files.py) can be set through the command line
|
||||||
|
or the environment variable RIPPLED_SIDECHAIN_CFG_DIR
|
||||||
|
'''
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from multiprocessing import Process, Value
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from typing import Callable, Dict, List, Optional
|
||||||
|
|
||||||
|
from app import App, single_client_app, testnet_app, configs_for_testnet
|
||||||
|
from command import AccountInfo, AccountTx, LedgerAccept, LogLevel, Subscribe
|
||||||
|
from common import Account, Asset, eprint, disable_eprint, XRP
|
||||||
|
from config_file import ConfigFile
|
||||||
|
import interactive
|
||||||
|
from log_analyzer import convert_log
|
||||||
|
from test_utils import mc_wait_for_payment_detect, sc_wait_for_payment_detect, mc_connect_subscription, sc_connect_subscription
|
||||||
|
from transaction import AccountSet, Payment, SignerListSet, SetRegularKey, Ticket, Trust
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args_helper(parser: argparse.ArgumentParser):
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--debug_sidechain',
|
||||||
|
'-ds',
|
||||||
|
action='store_true',
|
||||||
|
help=('Mode to debug sidechain (prompt to run sidechain in gdb)'),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--debug_mainchain',
|
||||||
|
'-dm',
|
||||||
|
action='store_true',
|
||||||
|
help=('Mode to debug mainchain (prompt to run sidechain in gdb)'),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--exe_mainchain',
|
||||||
|
'-em',
|
||||||
|
help=('path to mainchain rippled executable'),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--exe_sidechain',
|
||||||
|
'-es',
|
||||||
|
help=('path to mainchain rippled executable'),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--cfgs_dir',
|
||||||
|
'-c',
|
||||||
|
help=
|
||||||
|
('path to configuration file dir (generated with create_config_files.py)'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--standalone',
|
||||||
|
'-a',
|
||||||
|
action='store_true',
|
||||||
|
help=('run standalone tests'),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--interactive',
|
||||||
|
'-i',
|
||||||
|
action='store_true',
|
||||||
|
help=('run interactive repl'),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--quiet',
|
||||||
|
'-q',
|
||||||
|
action='store_true',
|
||||||
|
help=('Disable printing errors (eprint disabled)'),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--verbose',
|
||||||
|
'-v',
|
||||||
|
action='store_true',
|
||||||
|
help=('Enable printing errors (eprint enabled)'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pauses are use for attaching debuggers and looking at logs are know checkpoints
|
||||||
|
parser.add_argument(
|
||||||
|
'--with_pauses',
|
||||||
|
'-p',
|
||||||
|
action='store_true',
|
||||||
|
help=
|
||||||
|
('Add pauses at certain checkpoints in tests until "enter" key is hit'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--hooks_dir',
|
||||||
|
help=('path to hooks dir'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser(description=('Test and debug sidechains'))
|
||||||
|
parse_args_helper(parser)
|
||||||
|
return parser.parse_known_args()[0]
|
||||||
|
|
||||||
|
|
||||||
|
class Params:
|
||||||
|
def __init__(self, *, configs_dir: Optional[str] = None):
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
self.debug_sidechain = False
|
||||||
|
if args.debug_sidechain:
|
||||||
|
self.debug_sidechain = args.debug_sidechain
|
||||||
|
self.debug_mainchain = False
|
||||||
|
if args.debug_mainchain:
|
||||||
|
self.debug_mainchain = arts.debug_mainchain
|
||||||
|
|
||||||
|
# Undocumented feature: if the environment variable RIPPLED_SIDECHAIN_RR is set, it is
|
||||||
|
# assumed to point to the rr executable. Sidechain server 0 will then be run under rr.
|
||||||
|
self.sidechain_rr = None
|
||||||
|
if 'RIPPLED_SIDECHAIN_RR' in os.environ:
|
||||||
|
self.sidechain_rr = os.environ['RIPPLED_SIDECHAIN_RR']
|
||||||
|
|
||||||
|
self.standalone = args.standalone
|
||||||
|
self.with_pauses = args.with_pauses
|
||||||
|
self.interactive = args.interactive
|
||||||
|
self.quiet = args.quiet
|
||||||
|
self.verbose = args.verbose
|
||||||
|
|
||||||
|
self.mainchain_exe = None
|
||||||
|
if 'RIPPLED_MAINCHAIN_EXE' in os.environ:
|
||||||
|
self.mainchain_exe = os.environ['RIPPLED_MAINCHAIN_EXE']
|
||||||
|
if args.exe_mainchain:
|
||||||
|
self.mainchain_exe = args.exe_mainchain
|
||||||
|
|
||||||
|
self.sidechain_exe = None
|
||||||
|
if 'RIPPLED_SIDECHAIN_EXE' in os.environ:
|
||||||
|
self.sidechain_exe = os.environ['RIPPLED_SIDECHAIN_EXE']
|
||||||
|
if args.exe_sidechain:
|
||||||
|
self.sidechain_exe = args.exe_sidechain
|
||||||
|
|
||||||
|
self.configs_dir = None
|
||||||
|
if 'RIPPLED_SIDECHAIN_CFG_DIR' in os.environ:
|
||||||
|
self.configs_dir = os.environ['RIPPLED_SIDECHAIN_CFG_DIR']
|
||||||
|
if args.cfgs_dir:
|
||||||
|
self.configs_dir = args.cfgs_dir
|
||||||
|
if configs_dir is not None:
|
||||||
|
self.configs_dir = configs_dir
|
||||||
|
|
||||||
|
self.hooks_dir = None
|
||||||
|
if 'RIPPLED_SIDECHAIN_HOOKS_DIR' in os.environ:
|
||||||
|
self.hooks_dir = os.environ['RIPPLED_SIDECHAIN_HOOKS_DIR']
|
||||||
|
if args.hooks_dir:
|
||||||
|
self.hooks_dir = args.hooks_dir
|
||||||
|
|
||||||
|
if not self.configs_dir:
|
||||||
|
self.mainchain_config = None
|
||||||
|
self.sidechain_config = None
|
||||||
|
self.sidechain_bootstrap_config = None
|
||||||
|
self.genesis_account = None
|
||||||
|
self.mc_door_account = None
|
||||||
|
self.user_account = None
|
||||||
|
self.sc_door_account = None
|
||||||
|
self.federators = None
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.standalone:
|
||||||
|
self.mainchain_config = ConfigFile(
|
||||||
|
file_name=f'{self.configs_dir}/main.no_shards.dog/rippled.cfg')
|
||||||
|
self.sidechain_config = ConfigFile(
|
||||||
|
file_name=
|
||||||
|
f'{self.configs_dir}/main.no_shards.dog.sidechain/rippled.cfg')
|
||||||
|
self.sidechain_bootstrap_config = ConfigFile(
|
||||||
|
file_name=
|
||||||
|
f'{self.configs_dir}/main.no_shards.dog.sidechain/sidechain_bootstrap.cfg'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.mainchain_config = ConfigFile(
|
||||||
|
file_name=
|
||||||
|
f'{self.configs_dir}/sidechain_testnet/main.no_shards.mainchain_0/rippled.cfg'
|
||||||
|
)
|
||||||
|
self.sidechain_config = ConfigFile(
|
||||||
|
file_name=
|
||||||
|
f'{self.configs_dir}/sidechain_testnet/sidechain_0/rippled.cfg'
|
||||||
|
)
|
||||||
|
self.sidechain_bootstrap_config = ConfigFile(
|
||||||
|
file_name=
|
||||||
|
f'{self.configs_dir}/sidechain_testnet/sidechain_0/sidechain_bootstrap.cfg'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.genesis_account = Account(
|
||||||
|
account_id='rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
|
||||||
|
secret_key='masterpassphrase',
|
||||||
|
nickname='genesis')
|
||||||
|
self.mc_door_account = Account(
|
||||||
|
account_id=self.sidechain_config.sidechain.mainchain_account,
|
||||||
|
secret_key=self.sidechain_bootstrap_config.sidechain.
|
||||||
|
mainchain_secret,
|
||||||
|
nickname='door')
|
||||||
|
self.user_account = Account(
|
||||||
|
account_id='rJynXY96Vuq6B58pST9K5Ak5KgJ2JcRsQy',
|
||||||
|
secret_key='snVsJfrr2MbVpniNiUU6EDMGBbtzN',
|
||||||
|
nickname='alice')
|
||||||
|
|
||||||
|
self.sc_door_account = Account(
|
||||||
|
account_id='rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
|
||||||
|
secret_key='masterpassphrase',
|
||||||
|
nickname='door')
|
||||||
|
self.federators = [
|
||||||
|
l.split()[1].strip() for l in
|
||||||
|
self.sidechain_bootstrap_config.sidechain_federators.get_lines()
|
||||||
|
]
|
||||||
|
|
||||||
|
def check_error(self) -> str:
|
||||||
|
'''
|
||||||
|
Check for errors. Return `None` if no errors,
|
||||||
|
otherwise return a string describing the error
|
||||||
|
'''
|
||||||
|
if not self.mainchain_exe:
|
||||||
|
return 'Missing mainchain_exe location. Either set the env variable RIPPLED_MAINCHAIN_EXE or use the --exe_mainchain command line switch'
|
||||||
|
if not self.sidechain_exe:
|
||||||
|
return 'Missing sidechain_exe location. Either set the env variable RIPPLED_SIDECHAIN_EXE or use the --exe_sidechain command line switch'
|
||||||
|
if not self.configs_dir:
|
||||||
|
return 'Missing configs directory location. Either set the env variable RIPPLED_SIDECHAIN_CFG_DIR or use the --cfgs_dir command line switch'
|
||||||
|
if self.verbose and self.quiet:
|
||||||
|
return 'Cannot specify both verbose and quiet options at the same time'
|
||||||
|
|
||||||
|
|
||||||
|
mainDoorKeeper = 0
|
||||||
|
sideDoorKeeper = 1
|
||||||
|
updateSignerList = 2
|
||||||
|
|
||||||
|
|
||||||
|
def setup_mainchain(mc_app: App,
|
||||||
|
params: Params,
|
||||||
|
setup_user_accounts: bool = True):
|
||||||
|
mc_app.add_to_keymanager(params.mc_door_account)
|
||||||
|
if setup_user_accounts:
|
||||||
|
mc_app.add_to_keymanager(params.user_account)
|
||||||
|
|
||||||
|
mc_app(LogLevel('fatal'))
|
||||||
|
|
||||||
|
# Allow rippling through the genesis account
|
||||||
|
mc_app(AccountSet(account=params.genesis_account).set_default_ripple(True))
|
||||||
|
mc_app.maybe_ledger_accept()
|
||||||
|
|
||||||
|
# Create and fund the mc door account
|
||||||
|
mc_app(
|
||||||
|
Payment(account=params.genesis_account,
|
||||||
|
dst=params.mc_door_account,
|
||||||
|
amt=XRP(10_000)))
|
||||||
|
mc_app.maybe_ledger_accept()
|
||||||
|
|
||||||
|
# Create a trust line so USD/root account ious can be sent cross chain
|
||||||
|
mc_app(
|
||||||
|
Trust(account=params.mc_door_account,
|
||||||
|
limit_amt=Asset(value=1_000_000,
|
||||||
|
currency='USD',
|
||||||
|
issuer=params.genesis_account)))
|
||||||
|
|
||||||
|
# set the chain's signer list and disable the master key
|
||||||
|
divide = 4 * len(params.federators)
|
||||||
|
by = 5
|
||||||
|
quorum = (divide + by - 1) // by
|
||||||
|
mc_app(
|
||||||
|
SignerListSet(account=params.mc_door_account,
|
||||||
|
quorum=quorum,
|
||||||
|
keys=params.federators))
|
||||||
|
mc_app.maybe_ledger_accept()
|
||||||
|
r = mc_app(Ticket(account=params.mc_door_account, src_tag=mainDoorKeeper))
|
||||||
|
mc_app.maybe_ledger_accept()
|
||||||
|
mc_app(Ticket(account=params.mc_door_account, src_tag=sideDoorKeeper))
|
||||||
|
mc_app.maybe_ledger_accept()
|
||||||
|
mc_app(Ticket(account=params.mc_door_account, src_tag=updateSignerList))
|
||||||
|
mc_app.maybe_ledger_accept()
|
||||||
|
mc_app(AccountSet(account=params.mc_door_account).set_disable_master())
|
||||||
|
mc_app.maybe_ledger_accept()
|
||||||
|
|
||||||
|
if setup_user_accounts:
|
||||||
|
# Create and fund a regular user account
|
||||||
|
mc_app(
|
||||||
|
Payment(account=params.genesis_account,
|
||||||
|
dst=params.user_account,
|
||||||
|
amt=XRP(2_000)))
|
||||||
|
mc_app.maybe_ledger_accept()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_sidechain(sc_app: App,
|
||||||
|
params: Params,
|
||||||
|
setup_user_accounts: bool = True):
|
||||||
|
sc_app.add_to_keymanager(params.sc_door_account)
|
||||||
|
if setup_user_accounts:
|
||||||
|
sc_app.add_to_keymanager(params.user_account)
|
||||||
|
|
||||||
|
sc_app(LogLevel('fatal'))
|
||||||
|
sc_app(LogLevel('trace', partition='SidechainFederator'))
|
||||||
|
|
||||||
|
# set the chain's signer list and disable the master key
|
||||||
|
divide = 4 * len(params.federators)
|
||||||
|
by = 5
|
||||||
|
quorum = (divide + by - 1) // by
|
||||||
|
sc_app(
|
||||||
|
SignerListSet(account=params.genesis_account,
|
||||||
|
quorum=quorum,
|
||||||
|
keys=params.federators))
|
||||||
|
sc_app.maybe_ledger_accept()
|
||||||
|
sc_app(Ticket(account=params.genesis_account, src_tag=mainDoorKeeper))
|
||||||
|
sc_app.maybe_ledger_accept()
|
||||||
|
sc_app(Ticket(account=params.genesis_account, src_tag=sideDoorKeeper))
|
||||||
|
sc_app.maybe_ledger_accept()
|
||||||
|
sc_app(Ticket(account=params.genesis_account, src_tag=updateSignerList))
|
||||||
|
sc_app.maybe_ledger_accept()
|
||||||
|
sc_app(AccountSet(account=params.genesis_account).set_disable_master())
|
||||||
|
sc_app.maybe_ledger_accept()
|
||||||
|
|
||||||
|
|
||||||
|
def _xchain_transfer(from_chain: App, to_chain: App, src: Account,
|
||||||
|
dst: Account, amt: Asset, from_chain_door: Account,
|
||||||
|
to_chain_door: Account):
|
||||||
|
memos = [{'Memo': {'MemoData': dst.account_id_str_as_hex()}}]
|
||||||
|
from_chain(Payment(account=src, dst=from_chain_door, amt=amt, memos=memos))
|
||||||
|
from_chain.maybe_ledger_accept()
|
||||||
|
if to_chain.standalone:
|
||||||
|
# from_chain (side chain) sends a txn, but won't close the to_chain (main chain) ledger
|
||||||
|
time.sleep(1)
|
||||||
|
to_chain.maybe_ledger_accept()
|
||||||
|
|
||||||
|
|
||||||
|
def main_to_side_transfer(mc_app: App, sc_app: App, src: Account, dst: Account,
|
||||||
|
amt: Asset, params: Params):
|
||||||
|
_xchain_transfer(mc_app, sc_app, src, dst, amt, params.mc_door_account,
|
||||||
|
params.sc_door_account)
|
||||||
|
|
||||||
|
|
||||||
|
def side_to_main_transfer(mc_app: App, sc_app: App, src: Account, dst: Account,
|
||||||
|
amt: Asset, params: Params):
|
||||||
|
_xchain_transfer(sc_app, mc_app, src, dst, amt, params.sc_door_account,
|
||||||
|
params.mc_door_account)
|
||||||
|
|
||||||
|
|
||||||
|
def simple_test(mc_app: App, sc_app: App, params: Params):
|
||||||
|
try:
|
||||||
|
bob = sc_app.create_account('bob')
|
||||||
|
main_to_side_transfer(mc_app, sc_app, params.user_account, bob,
|
||||||
|
XRP(200), params)
|
||||||
|
main_to_side_transfer(mc_app, sc_app, params.user_account, bob,
|
||||||
|
XRP(60), params)
|
||||||
|
|
||||||
|
if params.with_pauses:
|
||||||
|
_convert_log_files_to_json(
|
||||||
|
mc_app.get_configs() + sc_app.get_configs(),
|
||||||
|
'checkpoint1.json')
|
||||||
|
input(
|
||||||
|
"Pausing to check for main -> side txns (press enter to continue)"
|
||||||
|
)
|
||||||
|
|
||||||
|
side_to_main_transfer(mc_app, sc_app, bob, params.user_account, XRP(9),
|
||||||
|
params)
|
||||||
|
side_to_main_transfer(mc_app, sc_app, bob, params.user_account,
|
||||||
|
XRP(11), params)
|
||||||
|
|
||||||
|
if params.with_pauses:
|
||||||
|
input(
|
||||||
|
"Pausing to check for side -> main txns (press enter to continue)"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
_convert_log_files_to_json(mc_app.get_configs() + sc_app.get_configs(),
|
||||||
|
'final.json')
|
||||||
|
|
||||||
|
|
||||||
|
def _rm_debug_log(config: ConfigFile):
|
||||||
|
try:
|
||||||
|
debug_log = config.debug_logfile.get_line()
|
||||||
|
if debug_log:
|
||||||
|
print(f'removing debug file: {debug_log}', flush=True)
|
||||||
|
os.remove(debug_log)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _standalone_with_callback(params: Params,
|
||||||
|
callback: Callable[[App, App], None],
|
||||||
|
setup_user_accounts: bool = True):
|
||||||
|
|
||||||
|
if (params.debug_mainchain):
|
||||||
|
input("Start mainchain server and press enter to continue: ")
|
||||||
|
else:
|
||||||
|
_rm_debug_log(params.mainchain_config)
|
||||||
|
with single_client_app(config=params.mainchain_config,
|
||||||
|
exe=params.mainchain_exe,
|
||||||
|
standalone=True,
|
||||||
|
run_server=not params.debug_mainchain) as mc_app:
|
||||||
|
|
||||||
|
mc_connect_subscription(mc_app, params.mc_door_account)
|
||||||
|
setup_mainchain(mc_app, params, setup_user_accounts)
|
||||||
|
|
||||||
|
if (params.debug_sidechain):
|
||||||
|
input("Start sidechain server and press enter to continue: ")
|
||||||
|
else:
|
||||||
|
_rm_debug_log(params.sidechain_config)
|
||||||
|
with single_client_app(
|
||||||
|
config=params.sidechain_config,
|
||||||
|
exe=params.sidechain_exe,
|
||||||
|
standalone=True,
|
||||||
|
run_server=not params.debug_sidechain) as sc_app:
|
||||||
|
|
||||||
|
sc_connect_subscription(sc_app, params.sc_door_account)
|
||||||
|
setup_sidechain(sc_app, params, setup_user_accounts)
|
||||||
|
callback(mc_app, sc_app)
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_log_files_to_json(to_convert: List[ConfigFile], suffix: str):
|
||||||
|
'''
|
||||||
|
Convert the log file to json
|
||||||
|
'''
|
||||||
|
for c in to_convert:
|
||||||
|
try:
|
||||||
|
debug_log = c.debug_logfile.get_line()
|
||||||
|
if not os.path.exists(debug_log):
|
||||||
|
continue
|
||||||
|
converted_log = f'{debug_log}.{suffix}'
|
||||||
|
if os.path.exists(converted_log):
|
||||||
|
os.remove(converted_log)
|
||||||
|
print(f'Converting log {debug_log} to {converted_log}', flush=True)
|
||||||
|
convert_log(debug_log, converted_log, pure_json=True)
|
||||||
|
except:
|
||||||
|
eprint(f'Exception converting log')
|
||||||
|
|
||||||
|
|
||||||
|
def _multinode_with_callback(params: Params,
|
||||||
|
callback: Callable[[App, App], None],
|
||||||
|
setup_user_accounts: bool = True):
|
||||||
|
|
||||||
|
mainchain_cfg = ConfigFile(
|
||||||
|
file_name=
|
||||||
|
f'{params.configs_dir}/sidechain_testnet/main.no_shards.mainchain_0/rippled.cfg'
|
||||||
|
)
|
||||||
|
_rm_debug_log(mainchain_cfg)
|
||||||
|
if params.debug_mainchain:
|
||||||
|
input("Start mainchain server and press enter to continue: ")
|
||||||
|
with single_client_app(config=mainchain_cfg,
|
||||||
|
exe=params.mainchain_exe,
|
||||||
|
standalone=True,
|
||||||
|
run_server=not params.debug_mainchain) as mc_app:
|
||||||
|
|
||||||
|
if params.with_pauses:
|
||||||
|
input("Pausing after mainchain start (press enter to continue)")
|
||||||
|
|
||||||
|
mc_connect_subscription(mc_app, params.mc_door_account)
|
||||||
|
setup_mainchain(mc_app, params, setup_user_accounts)
|
||||||
|
if params.with_pauses:
|
||||||
|
input("Pausing after mainchain setup (press enter to continue)")
|
||||||
|
|
||||||
|
testnet_configs = configs_for_testnet(
|
||||||
|
f'{params.configs_dir}/sidechain_testnet/sidechain_')
|
||||||
|
for c in testnet_configs:
|
||||||
|
_rm_debug_log(c)
|
||||||
|
|
||||||
|
run_server_list = [True] * len(testnet_configs)
|
||||||
|
if params.debug_sidechain:
|
||||||
|
run_server_list[0] = False
|
||||||
|
input(
|
||||||
|
f'Start testnet server {testnet_configs[0].get_file_name()} and press enter to continue: '
|
||||||
|
)
|
||||||
|
|
||||||
|
with testnet_app(exe=params.sidechain_exe,
|
||||||
|
configs=testnet_configs,
|
||||||
|
run_server=run_server_list,
|
||||||
|
sidechain_rr=params.sidechain_rr) as n_app:
|
||||||
|
|
||||||
|
if params.with_pauses:
|
||||||
|
input("Pausing after testnet start (press enter to continue)")
|
||||||
|
|
||||||
|
sc_connect_subscription(n_app, params.sc_door_account)
|
||||||
|
setup_sidechain(n_app, params, setup_user_accounts)
|
||||||
|
if params.with_pauses:
|
||||||
|
input(
|
||||||
|
"Pausing after sidechain setup (press enter to continue)")
|
||||||
|
callback(mc_app, n_app)
|
||||||
|
|
||||||
|
|
||||||
|
def standalone_test(params: Params):
|
||||||
|
def callback(mc_app: App, sc_app: App):
|
||||||
|
simple_test(mc_app, sc_app, params)
|
||||||
|
|
||||||
|
_standalone_with_callback(params, callback)
|
||||||
|
|
||||||
|
|
||||||
|
def multinode_test(params: Params):
|
||||||
|
def callback(mc_app: App, sc_app: App):
|
||||||
|
simple_test(mc_app, sc_app, params)
|
||||||
|
|
||||||
|
_multinode_with_callback(params, callback)
|
||||||
|
|
||||||
|
|
||||||
|
# The mainchain runs in standalone mode. Most operations - like cross chain
|
||||||
|
# paymens - will automatically close ledgers. However, some operations, like
|
||||||
|
# refunds need an extra close. This loop automatically closes ledgers.
|
||||||
|
def close_mainchain_ledgers(stop_token: Value, params: Params, sleep_time=4):
|
||||||
|
with single_client_app(config=params.mainchain_config,
|
||||||
|
exe=params.mainchain_exe,
|
||||||
|
standalone=True,
|
||||||
|
run_server=False) as mc_app:
|
||||||
|
while stop_token.value != 0:
|
||||||
|
mc_app.maybe_ledger_accept()
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
|
|
||||||
|
def standalone_interactive_repl(params: Params):
|
||||||
|
def callback(mc_app: App, sc_app: App):
|
||||||
|
# process will run while stop token is non-zero
|
||||||
|
stop_token = Value('i', 1)
|
||||||
|
p = None
|
||||||
|
if mc_app.standalone:
|
||||||
|
p = Process(target=close_mainchain_ledgers,
|
||||||
|
args=(stop_token, params))
|
||||||
|
p.start()
|
||||||
|
try:
|
||||||
|
interactive.repl(mc_app, sc_app)
|
||||||
|
finally:
|
||||||
|
if p:
|
||||||
|
stop_token.value = 0
|
||||||
|
p.join()
|
||||||
|
|
||||||
|
_standalone_with_callback(params, callback, setup_user_accounts=False)
|
||||||
|
|
||||||
|
|
||||||
|
def multinode_interactive_repl(params: Params):
|
||||||
|
def callback(mc_app: App, sc_app: App):
|
||||||
|
# process will run while stop token is non-zero
|
||||||
|
stop_token = Value('i', 1)
|
||||||
|
p = None
|
||||||
|
if mc_app.standalone:
|
||||||
|
p = Process(target=close_mainchain_ledgers,
|
||||||
|
args=(stop_token, params))
|
||||||
|
p.start()
|
||||||
|
try:
|
||||||
|
interactive.repl(mc_app, sc_app)
|
||||||
|
finally:
|
||||||
|
if p:
|
||||||
|
stop_token.value = 0
|
||||||
|
p.join()
|
||||||
|
|
||||||
|
_multinode_with_callback(params, callback, setup_user_accounts=False)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
params = Params()
|
||||||
|
interactive.set_hooks_dir(params.hooks_dir)
|
||||||
|
|
||||||
|
if err_str := params.check_error():
|
||||||
|
eprint(err_str)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if params.quiet:
|
||||||
|
print("Disabling eprint")
|
||||||
|
disable_eprint()
|
||||||
|
|
||||||
|
if params.interactive:
|
||||||
|
if params.standalone:
|
||||||
|
standalone_interactive_repl(params)
|
||||||
|
else:
|
||||||
|
multinode_interactive_repl(params)
|
||||||
|
elif params.standalone:
|
||||||
|
standalone_test(params)
|
||||||
|
else:
|
||||||
|
multinode_test(params)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
176
bin/sidechain/python/test_utils.py
Normal file
176
bin/sidechain/python/test_utils.py
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import asyncio
|
||||||
|
import collections
|
||||||
|
from contextlib import contextmanager
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import pprint
|
||||||
|
import time
|
||||||
|
from typing import Callable, Dict, List, Optional
|
||||||
|
|
||||||
|
from app import App, balances_dataframe
|
||||||
|
from common import Account, Asset, XRP, eprint
|
||||||
|
from command import Subscribe
|
||||||
|
|
||||||
|
MC_SUBSCRIBE_QUEUE = []
|
||||||
|
SC_SUBSCRIBE_QUEUE = []
|
||||||
|
|
||||||
|
|
||||||
|
def _mc_subscribe_callback(v: dict):
|
||||||
|
MC_SUBSCRIBE_QUEUE.append(v)
|
||||||
|
logging.info(f'mc subscribe_callback:\n{json.dumps(v, indent=1)}')
|
||||||
|
|
||||||
|
|
||||||
|
def _sc_subscribe_callback(v: dict):
|
||||||
|
SC_SUBSCRIBE_QUEUE.append(v)
|
||||||
|
logging.info(f'sc subscribe_callback:\n{json.dumps(v, indent=1)}')
|
||||||
|
|
||||||
|
|
||||||
|
def mc_connect_subscription(app: App, door_account: Account):
|
||||||
|
app(Subscribe(account_history_account=door_account),
|
||||||
|
_mc_subscribe_callback)
|
||||||
|
|
||||||
|
|
||||||
|
def sc_connect_subscription(app: App, door_account: Account):
|
||||||
|
app(Subscribe(account_history_account=door_account),
|
||||||
|
_sc_subscribe_callback)
|
||||||
|
|
||||||
|
|
||||||
|
# This pops elements off the subscribe_queue until the transaction is found
|
||||||
|
# It mofifies the queue in place.
|
||||||
|
async def async_wait_for_payment_detect(app: App, subscribe_queue: List[dict],
|
||||||
|
src: Account, dst: Account,
|
||||||
|
amt_asset: Asset):
|
||||||
|
logging.info(
|
||||||
|
f'Wait for payment {src.account_id = } {dst.account_id = } {amt_asset = }'
|
||||||
|
)
|
||||||
|
n_txns = 10 # keep this many txn in a circular buffer.
|
||||||
|
# If the payment is not detected, write them to the log.
|
||||||
|
last_n_paytxns = collections.deque(maxlen=n_txns)
|
||||||
|
for i in range(30):
|
||||||
|
while subscribe_queue:
|
||||||
|
d = subscribe_queue.pop(0)
|
||||||
|
if 'transaction' not in d:
|
||||||
|
continue
|
||||||
|
txn = d['transaction']
|
||||||
|
if txn['TransactionType'] != 'Payment':
|
||||||
|
continue
|
||||||
|
|
||||||
|
txn_asset = Asset(from_rpc_result=txn['Amount'])
|
||||||
|
if txn['Account'] == src.account_id and txn[
|
||||||
|
'Destination'] == dst.account_id and txn_asset == amt_asset:
|
||||||
|
if d['engine_result_code'] == 0:
|
||||||
|
logging.info(
|
||||||
|
f'Found payment {src.account_id = } {dst.account_id = } {amt_asset = }'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logging.error(
|
||||||
|
f'Expected payment failed {src.account_id = } {dst.account_id = } {amt_asset = }'
|
||||||
|
)
|
||||||
|
raise ValueError(
|
||||||
|
f'Expected payment failed {src.account_id = } {dst.account_id = } {amt_asset = }'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
last_n_paytxns.append(txn)
|
||||||
|
if i > 0 and not (i % 5):
|
||||||
|
logging.warning(
|
||||||
|
f'Waiting for txn detect {src.account_id = } {dst.account_id = } {amt_asset = }'
|
||||||
|
)
|
||||||
|
# side chain can send transactions to the main chain, but won't close the ledger
|
||||||
|
# We don't know when the transaction will be sent, so may need to close the ledger here
|
||||||
|
await app.async_maybe_ledger_accept()
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
logging.warning(
|
||||||
|
f'Last {len(last_n_paytxns)} pay txns while waiting for payment detect'
|
||||||
|
)
|
||||||
|
for t in last_n_paytxns:
|
||||||
|
logging.warning(
|
||||||
|
f'Detected pay transaction while waiting for payment: {t}')
|
||||||
|
logging.error(
|
||||||
|
f'Expected txn detect {src.account_id = } {dst.account_id = } {amt_asset = }'
|
||||||
|
)
|
||||||
|
raise ValueError(
|
||||||
|
f'Expected txn detect {src.account_id = } {dst.account_id = } {amt_asset = }'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mc_wait_for_payment_detect(app: App, src: Account, dst: Account,
|
||||||
|
amt_asset: Asset):
|
||||||
|
logging.info(f'mainchain waiting for payment detect')
|
||||||
|
return asyncio.get_event_loop().run_until_complete(
|
||||||
|
async_wait_for_payment_detect(app, MC_SUBSCRIBE_QUEUE, src, dst,
|
||||||
|
amt_asset))
|
||||||
|
|
||||||
|
|
||||||
|
def sc_wait_for_payment_detect(app: App, src: Account, dst: Account,
|
||||||
|
amt_asset: Asset):
|
||||||
|
logging.info(f'sidechain waiting for payment detect')
|
||||||
|
return asyncio.get_event_loop().run_until_complete(
|
||||||
|
async_wait_for_payment_detect(app, SC_SUBSCRIBE_QUEUE, src, dst,
|
||||||
|
amt_asset))
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_balance_change(app: App,
|
||||||
|
acc: Account,
|
||||||
|
pre_balance: Asset,
|
||||||
|
expected_diff: Optional[Asset] = None):
|
||||||
|
logging.info(
|
||||||
|
f'waiting for balance change {acc.account_id = } {pre_balance = } {expected_diff = }'
|
||||||
|
)
|
||||||
|
for i in range(30):
|
||||||
|
new_bal = app.get_balance(acc, pre_balance(0))
|
||||||
|
diff = new_bal - pre_balance
|
||||||
|
if new_bal != pre_balance:
|
||||||
|
logging.info(
|
||||||
|
f'Balance changed {acc.account_id = } {pre_balance = } {new_bal = } {diff = } {expected_diff = }'
|
||||||
|
)
|
||||||
|
if expected_diff is None or diff == expected_diff:
|
||||||
|
return
|
||||||
|
app.maybe_ledger_accept()
|
||||||
|
time.sleep(2)
|
||||||
|
if i > 0 and not (i % 5):
|
||||||
|
logging.warning(
|
||||||
|
f'Waiting for balance to change {acc.account_id = } {pre_balance = }'
|
||||||
|
)
|
||||||
|
logging.error(
|
||||||
|
f'Expected balance to change {acc.account_id = } {pre_balance = } {new_bal = } {diff = } {expected_diff = }'
|
||||||
|
)
|
||||||
|
raise ValueError(
|
||||||
|
f'Expected balance to change {acc.account_id = } {pre_balance = } {new_bal = } {diff = } {expected_diff = }'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def log_chain_state(mc_app, sc_app, log, msg='Chain State'):
|
||||||
|
chains = [mc_app, sc_app]
|
||||||
|
chain_names = ['mainchain', 'sidechain']
|
||||||
|
balances = balances_dataframe(chains, chain_names)
|
||||||
|
df_as_str = balances.to_string(float_format=lambda x: f'{x:,.6f}')
|
||||||
|
log(f'{msg} Balances: \n{df_as_str}')
|
||||||
|
federator_info = sc_app.federator_info()
|
||||||
|
log(f'{msg} Federator Info: \n{pprint.pformat(federator_info)}')
|
||||||
|
|
||||||
|
|
||||||
|
# Tests can set this to True to help debug test failures by showing account
|
||||||
|
# balances in the log before the test runs
|
||||||
|
test_context_verbose_logging = False
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def test_context(mc_app, sc_app, verbose_logging: Optional[bool] = None):
|
||||||
|
'''Write extra context info to the log on test failure'''
|
||||||
|
global test_context_verbose_logging
|
||||||
|
if verbose_logging is None:
|
||||||
|
verbose_logging = test_context_verbose_logging
|
||||||
|
try:
|
||||||
|
if verbose_logging:
|
||||||
|
log_chain_state(mc_app, sc_app, logging.info)
|
||||||
|
start_time = time.monotonic()
|
||||||
|
yield
|
||||||
|
except:
|
||||||
|
log_chain_state(mc_app, sc_app, logging.error)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
elapased_time = time.monotonic() - start_time
|
||||||
|
logging.info(f'Test elapsed time: {elapased_time}')
|
||||||
|
if verbose_logging:
|
||||||
|
log_chain_state(mc_app, sc_app, logging.info)
|
||||||
216
bin/sidechain/python/testnet.py
Normal file
216
bin/sidechain/python/testnet.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
'''
|
||||||
|
Bring up a rippled testnetwork from a set of config files with fixed ips.
|
||||||
|
'''
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from typing import Callable, List, Optional, Set, Union
|
||||||
|
|
||||||
|
from command import ServerInfo
|
||||||
|
from config_file import ConfigFile
|
||||||
|
from ripple_client import RippleClient
|
||||||
|
|
||||||
|
|
||||||
|
class Network:
|
||||||
|
# If run_server is None, run all the servers.
|
||||||
|
# This is useful to help debugging
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
exe: str,
|
||||||
|
configs: List[ConfigFile],
|
||||||
|
*,
|
||||||
|
command_logs: Optional[List[str]] = None,
|
||||||
|
run_server: Optional[List[bool]] = None,
|
||||||
|
# undocumented feature. If with_rr is not None, assume it points to the rr debugger executable
|
||||||
|
# and run server 0 under rr
|
||||||
|
with_rr: Optional[str] = None,
|
||||||
|
extra_args: Optional[List[List[str]]] = None):
|
||||||
|
|
||||||
|
self.with_rr = with_rr
|
||||||
|
if not configs:
|
||||||
|
raise ValueError(f'Must specify at least one config')
|
||||||
|
|
||||||
|
if run_server and len(run_server) != len(configs):
|
||||||
|
raise ValueError(
|
||||||
|
f'run_server length must match number of configs (or be None): {len(configs) = } {len(run_server) = }'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.configs = configs
|
||||||
|
self.clients = []
|
||||||
|
self.running_server_indexes = set()
|
||||||
|
self.processes = {}
|
||||||
|
|
||||||
|
if not run_server:
|
||||||
|
run_server = []
|
||||||
|
run_server += [True] * (len(configs) - len(run_server))
|
||||||
|
|
||||||
|
self.run_server = run_server
|
||||||
|
|
||||||
|
if not command_logs:
|
||||||
|
command_logs = []
|
||||||
|
command_logs += [None] * (len(configs) - len(command_logs))
|
||||||
|
|
||||||
|
self.command_logs = command_logs
|
||||||
|
|
||||||
|
# remove the old database directories.
|
||||||
|
# we want tests to start from the same empty state every time
|
||||||
|
for config in self.configs:
|
||||||
|
db_path = config.database_path.get_line()
|
||||||
|
if db_path and os.path.isdir(db_path):
|
||||||
|
files = glob.glob(f'{db_path}/**', recursive=True)
|
||||||
|
for f in files:
|
||||||
|
if os.path.isdir(f):
|
||||||
|
continue
|
||||||
|
os.unlink(f)
|
||||||
|
|
||||||
|
for config, log in zip(self.configs, self.command_logs):
|
||||||
|
client = RippleClient(config=config, command_log=log, exe=exe)
|
||||||
|
self.clients.append(client)
|
||||||
|
|
||||||
|
self.servers_start(extra_args=extra_args)
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
for a in self.clients:
|
||||||
|
a.shutdown()
|
||||||
|
|
||||||
|
self.servers_stop()
|
||||||
|
|
||||||
|
def num_clients(self) -> int:
|
||||||
|
return len(self.clients)
|
||||||
|
|
||||||
|
def get_client(self, i: int) -> RippleClient:
|
||||||
|
return self.clients[i]
|
||||||
|
|
||||||
|
def get_configs(self) -> List[ConfigFile]:
|
||||||
|
return [c.config for c in self.clients]
|
||||||
|
|
||||||
|
def get_pids(self) -> List[int]:
|
||||||
|
return [c.get_pid() for c in self.clients if c.get_pid() is not None]
|
||||||
|
|
||||||
|
# Get a dict of the server_state, validated_ledger_seq, and complete_ledgers
|
||||||
|
def get_brief_server_info(self) -> dict:
|
||||||
|
ret = {'server_state': [], 'ledger_seq': [], 'complete_ledgers': []}
|
||||||
|
for c in self.clients:
|
||||||
|
r = c.get_brief_server_info()
|
||||||
|
for (k, v) in r.items():
|
||||||
|
ret[k].append(v)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
# returns true if the server is running, false if not. Note, this relies on
|
||||||
|
# servers being shut down through the `servers_stop` interface. If a server
|
||||||
|
# crashes, or is started or stopped through other means, an incorrect status
|
||||||
|
# may be reported.
|
||||||
|
def get_running_status(self) -> List[bool]:
|
||||||
|
return [
|
||||||
|
i in self.running_server_indexes for i in range(len(self.clients))
|
||||||
|
]
|
||||||
|
|
||||||
|
def is_running(self, index: int) -> bool:
|
||||||
|
return index in self.running_server_indexes
|
||||||
|
|
||||||
|
def wait_for_validated_ledger(self, server_index: Optional[int] = None):
|
||||||
|
'''
|
||||||
|
Don't return until the network has at least one validated ledger
|
||||||
|
'''
|
||||||
|
|
||||||
|
if server_index is None:
|
||||||
|
for i in range(len(self.configs)):
|
||||||
|
self.wait_for_validated_ledger(i)
|
||||||
|
return
|
||||||
|
|
||||||
|
client = self.clients[server_index]
|
||||||
|
for i in range(600):
|
||||||
|
r = client.send_command(ServerInfo())
|
||||||
|
state = None
|
||||||
|
if 'info' in r:
|
||||||
|
state = r['info']['server_state']
|
||||||
|
if state == 'proposing':
|
||||||
|
print(f'Synced: {server_index} : {state}', flush=True)
|
||||||
|
break
|
||||||
|
if not i % 10:
|
||||||
|
print(f'Waiting for sync: {server_index} : {state}',
|
||||||
|
flush=True)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
for i in range(600):
|
||||||
|
r = client.send_command(ServerInfo())
|
||||||
|
state = None
|
||||||
|
if 'info' in r:
|
||||||
|
complete_ledgers = r['info']['complete_ledgers']
|
||||||
|
if complete_ledgers and complete_ledgers != 'empty':
|
||||||
|
print(f'Have complete ledgers: {server_index} : {state}',
|
||||||
|
flush=True)
|
||||||
|
return
|
||||||
|
if not i % 10:
|
||||||
|
print(
|
||||||
|
f'Waiting for complete_ledgers: {server_index} : {complete_ledgers}',
|
||||||
|
flush=True)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
raise ValueError('Could not sync server {client.config_file_name}')
|
||||||
|
|
||||||
|
def servers_start(self,
|
||||||
|
server_indexes: Optional[Union[Set[int],
|
||||||
|
List[int]]] = None,
|
||||||
|
*,
|
||||||
|
extra_args: Optional[List[List[str]]] = None):
|
||||||
|
if server_indexes is None:
|
||||||
|
server_indexes = [i for i in range(len(self.clients))]
|
||||||
|
|
||||||
|
if extra_args is None:
|
||||||
|
extra_args = []
|
||||||
|
extra_args += [list()] * (len(self.configs) - len(extra_args))
|
||||||
|
|
||||||
|
for i in server_indexes:
|
||||||
|
if i in self.running_server_indexes or not self.run_server[i]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
client = self.clients[i]
|
||||||
|
to_run = [client.exe, '--conf', client.config_file_name]
|
||||||
|
if self.with_rr and i == 0:
|
||||||
|
to_run = [self.with_rr, 'record'] + to_run
|
||||||
|
print(f'Starting server with rr {client.config_file_name}')
|
||||||
|
else:
|
||||||
|
print(f'Starting server {client.config_file_name}')
|
||||||
|
fout = open(os.devnull, 'w')
|
||||||
|
p = subprocess.Popen(to_run + extra_args[i],
|
||||||
|
stdout=fout,
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
client.set_pid(p.pid)
|
||||||
|
print(
|
||||||
|
f'started rippled: config: {client.config_file_name} PID: {p.pid}',
|
||||||
|
flush=True)
|
||||||
|
self.running_server_indexes.add(i)
|
||||||
|
self.processes[i] = p
|
||||||
|
|
||||||
|
time.sleep(2) # give servers time to start
|
||||||
|
|
||||||
|
def servers_stop(self,
|
||||||
|
server_indexes: Optional[Union[Set[int],
|
||||||
|
List[int]]] = None):
|
||||||
|
if server_indexes is None:
|
||||||
|
server_indexes = self.running_server_indexes.copy()
|
||||||
|
|
||||||
|
if 0 in server_indexes:
|
||||||
|
print(
|
||||||
|
f'WARNING: Server 0 is being stopped. RPC commands cannot be sent until this is restarted.'
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in server_indexes:
|
||||||
|
if i not in self.running_server_indexes:
|
||||||
|
continue
|
||||||
|
client = self.clients[i]
|
||||||
|
to_run = [client.exe, '--conf', client.config_file_name]
|
||||||
|
fout = open(os.devnull, 'w')
|
||||||
|
subprocess.Popen(to_run + ['stop'],
|
||||||
|
stdout=fout,
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
self.running_server_indexes.discard(i)
|
||||||
|
|
||||||
|
for i in server_indexes:
|
||||||
|
self.processes[i].wait()
|
||||||
|
del self.processes[i]
|
||||||
|
self.get_client(i).set_pid(-1)
|
||||||
64
bin/sidechain/python/tests/conftest.py
Normal file
64
bin/sidechain/python/tests/conftest.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Add parent directory to module path
|
||||||
|
import os, sys
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from common import Account, Asset, XRP
|
||||||
|
import create_config_files
|
||||||
|
import sidechain
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
'''
|
||||||
|
Sidechains uses argparse.ArgumentParser to add command line options.
|
||||||
|
The function call to add an argument is `add_argument`. pytest uses `addoption`.
|
||||||
|
This wrapper class changes calls from `add_argument` to calls to `addoption`.
|
||||||
|
To avoid conflicts between pytest and sidechains, all sidechain arguments have
|
||||||
|
the suffix `_sc` appended to them. I.e. `--verbose` is for pytest, `--verbose_sc`
|
||||||
|
is for sidechains.
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
class ArgumentParserWrapper:
|
||||||
|
def __init__(self, wrapped):
|
||||||
|
self.wrapped = wrapped
|
||||||
|
|
||||||
|
def add_argument(self, *args, **kwargs):
|
||||||
|
for a in args:
|
||||||
|
if not a.startswith('--'):
|
||||||
|
continue
|
||||||
|
a = a + '_sc'
|
||||||
|
self.wrapped.addoption(a, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
wrapped = ArgumentParserWrapper(parser)
|
||||||
|
sidechain.parse_args_helper(wrapped)
|
||||||
|
|
||||||
|
|
||||||
|
def _xchain_assets(ratio: int = 1):
|
||||||
|
assets = {}
|
||||||
|
assets['xrp_xrp_sidechain_asset'] = create_config_files.XChainAsset(
|
||||||
|
XRP(0), XRP(0), 1, 1 * ratio, 200, 200 * ratio)
|
||||||
|
root_account = Account(account_id="rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh")
|
||||||
|
main_iou_asset = Asset(value=0, currency='USD', issuer=root_account)
|
||||||
|
side_iou_asset = Asset(value=0, currency='USD', issuer=root_account)
|
||||||
|
assets['iou_iou_sidechain_asset'] = create_config_files.XChainAsset(
|
||||||
|
main_iou_asset, side_iou_asset, 1, 1 * ratio, 0.02, 0.02 * ratio)
|
||||||
|
return assets
|
||||||
|
|
||||||
|
|
||||||
|
# Diction of config dirs. Key is ratio
|
||||||
|
_config_dirs = None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def configs_dirs_dict(tmp_path):
|
||||||
|
global _config_dirs
|
||||||
|
if not _config_dirs:
|
||||||
|
params = create_config_files.Params()
|
||||||
|
_config_dirs = {}
|
||||||
|
for ratio in (1, 2):
|
||||||
|
params.configs_dir = str(tmp_path / f'test_config_files_{ratio}')
|
||||||
|
create_config_files.main(params, _xchain_assets(ratio))
|
||||||
|
_config_dirs[ratio] = params.configs_dir
|
||||||
|
|
||||||
|
return _config_dirs
|
||||||
117
bin/sidechain/python/tests/door_test.py
Normal file
117
bin/sidechain/python/tests/door_test.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
from typing import Dict
|
||||||
|
from app import App
|
||||||
|
from common import XRP
|
||||||
|
from sidechain import Params
|
||||||
|
import sidechain
|
||||||
|
import test_utils
|
||||||
|
import time
|
||||||
|
from transaction import Payment
|
||||||
|
import tst_common
|
||||||
|
|
||||||
|
batch_test_num_accounts = 200
|
||||||
|
|
||||||
|
|
||||||
|
def door_test(mc_app: App, sc_app: App, params: Params):
|
||||||
|
# setup, create accounts on both chains
|
||||||
|
for i in range(batch_test_num_accounts):
|
||||||
|
name = "m_" + str(i)
|
||||||
|
account_main = mc_app.create_account(name)
|
||||||
|
name = "s_" + str(i)
|
||||||
|
account_side = sc_app.create_account(name)
|
||||||
|
mc_app(
|
||||||
|
Payment(account=params.genesis_account,
|
||||||
|
dst=account_main,
|
||||||
|
amt=XRP(20_000)))
|
||||||
|
mc_app.maybe_ledger_accept()
|
||||||
|
account_main_last = mc_app.account_from_alias("m_" +
|
||||||
|
str(batch_test_num_accounts -
|
||||||
|
1))
|
||||||
|
test_utils.wait_for_balance_change(mc_app, account_main_last, XRP(0),
|
||||||
|
XRP(20_000))
|
||||||
|
|
||||||
|
# test
|
||||||
|
to_side_xrp = XRP(1000)
|
||||||
|
to_main_xrp = XRP(100)
|
||||||
|
last_tx_xrp = XRP(343)
|
||||||
|
with test_utils.test_context(mc_app, sc_app, True):
|
||||||
|
# send xchain payment to open accounts on sidechain
|
||||||
|
for i in range(batch_test_num_accounts):
|
||||||
|
name_main = "m_" + str(i)
|
||||||
|
account_main = mc_app.account_from_alias(name_main)
|
||||||
|
name_side = "s_" + str(i)
|
||||||
|
account_side = sc_app.account_from_alias(name_side)
|
||||||
|
memos = [{
|
||||||
|
'Memo': {
|
||||||
|
'MemoData': account_side.account_id_str_as_hex()
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
mc_app(
|
||||||
|
Payment(account=account_main,
|
||||||
|
dst=params.mc_door_account,
|
||||||
|
amt=to_side_xrp,
|
||||||
|
memos=memos))
|
||||||
|
|
||||||
|
while 1:
|
||||||
|
federator_info = sc_app.federator_info()
|
||||||
|
should_loop = False
|
||||||
|
for v in federator_info.values():
|
||||||
|
for c in ['mainchain', 'sidechain']:
|
||||||
|
state = v['info'][c]['listener_info']['state']
|
||||||
|
if state != 'normal':
|
||||||
|
should_loop = True
|
||||||
|
if not should_loop:
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# wait some time for the door to change
|
||||||
|
door_closing = False
|
||||||
|
door_reopened = False
|
||||||
|
for i in range(batch_test_num_accounts * 2 + 40):
|
||||||
|
server_index = [0]
|
||||||
|
federator_info = sc_app.federator_info(server_index)
|
||||||
|
for v in federator_info.values():
|
||||||
|
door_status = v['info']['mainchain']['door_status']['status']
|
||||||
|
if not door_closing:
|
||||||
|
if door_status != 'open':
|
||||||
|
door_closing = True
|
||||||
|
else:
|
||||||
|
if door_status == 'open':
|
||||||
|
door_reopened = True
|
||||||
|
|
||||||
|
if not door_reopened:
|
||||||
|
time.sleep(1)
|
||||||
|
mc_app.maybe_ledger_accept()
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not door_reopened:
|
||||||
|
raise ValueError('Expected door status changes did not happen')
|
||||||
|
|
||||||
|
# wait for accounts created on sidechain
|
||||||
|
for i in range(batch_test_num_accounts):
|
||||||
|
name_side = "s_" + str(i)
|
||||||
|
account_side = sc_app.account_from_alias(name_side)
|
||||||
|
test_utils.wait_for_balance_change(sc_app, account_side, XRP(0),
|
||||||
|
to_side_xrp)
|
||||||
|
|
||||||
|
# # try one xchain payment, each direction
|
||||||
|
name_main = "m_" + str(0)
|
||||||
|
account_main = mc_app.account_from_alias(name_main)
|
||||||
|
name_side = "s_" + str(0)
|
||||||
|
account_side = sc_app.account_from_alias(name_side)
|
||||||
|
|
||||||
|
pre_bal = mc_app.get_balance(account_main, XRP(0))
|
||||||
|
sidechain.side_to_main_transfer(mc_app, sc_app, account_side,
|
||||||
|
account_main, to_main_xrp, params)
|
||||||
|
test_utils.wait_for_balance_change(mc_app, account_main, pre_bal,
|
||||||
|
to_main_xrp)
|
||||||
|
|
||||||
|
pre_bal = sc_app.get_balance(account_side, XRP(0))
|
||||||
|
sidechain.main_to_side_transfer(mc_app, sc_app, account_main,
|
||||||
|
account_side, last_tx_xrp, params)
|
||||||
|
test_utils.wait_for_balance_change(sc_app, account_side, pre_bal,
|
||||||
|
last_tx_xrp)
|
||||||
|
|
||||||
|
|
||||||
|
def test_door_operations(configs_dirs_dict: Dict[int, str]):
|
||||||
|
tst_common.test_start(configs_dirs_dict, door_test)
|
||||||
151
bin/sidechain/python/tests/simple_xchain_transfer_test.py
Normal file
151
bin/sidechain/python/tests/simple_xchain_transfer_test.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import logging
|
||||||
|
import pprint
|
||||||
|
import pytest
|
||||||
|
from multiprocessing import Process, Value
|
||||||
|
from typing import Dict
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from app import App
|
||||||
|
from common import Asset, eprint, disable_eprint, drops, XRP
|
||||||
|
import interactive
|
||||||
|
from sidechain import Params
|
||||||
|
import sidechain
|
||||||
|
import test_utils
|
||||||
|
import time
|
||||||
|
from transaction import Payment, Trust
|
||||||
|
import tst_common
|
||||||
|
|
||||||
|
|
||||||
|
def simple_xrp_test(mc_app: App, sc_app: App, params: Params):
|
||||||
|
alice = mc_app.account_from_alias('alice')
|
||||||
|
adam = sc_app.account_from_alias('adam')
|
||||||
|
mc_door = mc_app.account_from_alias('door')
|
||||||
|
sc_door = sc_app.account_from_alias('door')
|
||||||
|
|
||||||
|
# main to side
|
||||||
|
# First txn funds the side chain account
|
||||||
|
with test_utils.test_context(mc_app, sc_app):
|
||||||
|
to_send_asset = XRP(9999)
|
||||||
|
mc_pre_bal = mc_app.get_balance(mc_door, to_send_asset)
|
||||||
|
sc_pre_bal = sc_app.get_balance(adam, to_send_asset)
|
||||||
|
sidechain.main_to_side_transfer(mc_app, sc_app, alice, adam,
|
||||||
|
to_send_asset, params)
|
||||||
|
test_utils.wait_for_balance_change(mc_app, mc_door, mc_pre_bal,
|
||||||
|
to_send_asset)
|
||||||
|
test_utils.wait_for_balance_change(sc_app, adam, sc_pre_bal,
|
||||||
|
to_send_asset)
|
||||||
|
|
||||||
|
for i in range(2):
|
||||||
|
# even amounts for main to side
|
||||||
|
for value in range(20, 30, 2):
|
||||||
|
with test_utils.test_context(mc_app, sc_app):
|
||||||
|
to_send_asset = drops(value)
|
||||||
|
mc_pre_bal = mc_app.get_balance(mc_door, to_send_asset)
|
||||||
|
sc_pre_bal = sc_app.get_balance(adam, to_send_asset)
|
||||||
|
sidechain.main_to_side_transfer(mc_app, sc_app, alice, adam,
|
||||||
|
to_send_asset, params)
|
||||||
|
test_utils.wait_for_balance_change(mc_app, mc_door, mc_pre_bal,
|
||||||
|
to_send_asset)
|
||||||
|
test_utils.wait_for_balance_change(sc_app, adam, sc_pre_bal,
|
||||||
|
to_send_asset)
|
||||||
|
|
||||||
|
# side to main
|
||||||
|
# odd amounts for side to main
|
||||||
|
for value in range(19, 29, 2):
|
||||||
|
with test_utils.test_context(mc_app, sc_app):
|
||||||
|
to_send_asset = drops(value)
|
||||||
|
pre_bal = mc_app.get_balance(alice, to_send_asset)
|
||||||
|
sidechain.side_to_main_transfer(mc_app, sc_app, adam, alice,
|
||||||
|
to_send_asset, params)
|
||||||
|
test_utils.wait_for_balance_change(mc_app, alice, pre_bal,
|
||||||
|
to_send_asset)
|
||||||
|
|
||||||
|
|
||||||
|
def simple_iou_test(mc_app: App, sc_app: App, params: Params):
|
||||||
|
alice = mc_app.account_from_alias('alice')
|
||||||
|
adam = sc_app.account_from_alias('adam')
|
||||||
|
|
||||||
|
mc_asset = Asset(value=0,
|
||||||
|
currency='USD',
|
||||||
|
issuer=mc_app.account_from_alias('root'))
|
||||||
|
sc_asset = Asset(value=0,
|
||||||
|
currency='USD',
|
||||||
|
issuer=sc_app.account_from_alias('door'))
|
||||||
|
mc_app.add_asset_alias(mc_asset, 'mcd') # main chain dollar
|
||||||
|
sc_app.add_asset_alias(sc_asset, 'scd') # side chain dollar
|
||||||
|
mc_app(Trust(account=alice, limit_amt=mc_asset(1_000_000)))
|
||||||
|
|
||||||
|
## make sure adam account on the side chain exists and set the trust line
|
||||||
|
with test_utils.test_context(mc_app, sc_app):
|
||||||
|
sidechain.main_to_side_transfer(mc_app, sc_app, alice, adam, XRP(300),
|
||||||
|
params)
|
||||||
|
|
||||||
|
# create a trust line to alice and pay her USD/root
|
||||||
|
mc_app(Trust(account=alice, limit_amt=mc_asset(1_000_000)))
|
||||||
|
mc_app.maybe_ledger_accept()
|
||||||
|
mc_app(
|
||||||
|
Payment(account=mc_app.account_from_alias('root'),
|
||||||
|
dst=alice,
|
||||||
|
amt=mc_asset(10_000)))
|
||||||
|
mc_app.maybe_ledger_accept()
|
||||||
|
|
||||||
|
# create a trust line for adam
|
||||||
|
sc_app(Trust(account=adam, limit_amt=sc_asset(1_000_000)))
|
||||||
|
|
||||||
|
for i in range(2):
|
||||||
|
# even amounts for main to side
|
||||||
|
for value in range(10, 20, 2):
|
||||||
|
with test_utils.test_context(mc_app, sc_app):
|
||||||
|
to_send_asset = mc_asset(value)
|
||||||
|
rcv_asset = sc_asset(value)
|
||||||
|
pre_bal = sc_app.get_balance(adam, rcv_asset)
|
||||||
|
sidechain.main_to_side_transfer(mc_app, sc_app, alice, adam,
|
||||||
|
to_send_asset, params)
|
||||||
|
test_utils.wait_for_balance_change(sc_app, adam, pre_bal,
|
||||||
|
rcv_asset)
|
||||||
|
|
||||||
|
# side to main
|
||||||
|
# odd amounts for side to main
|
||||||
|
for value in range(9, 19, 2):
|
||||||
|
with test_utils.test_context(mc_app, sc_app):
|
||||||
|
to_send_asset = sc_asset(value)
|
||||||
|
rcv_asset = mc_asset(value)
|
||||||
|
pre_bal = mc_app.get_balance(alice, to_send_asset)
|
||||||
|
sidechain.side_to_main_transfer(mc_app, sc_app, adam, alice,
|
||||||
|
to_send_asset, params)
|
||||||
|
test_utils.wait_for_balance_change(mc_app, alice, pre_bal,
|
||||||
|
rcv_asset)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_accounts(mc_app: App, sc_app: App, params: Params):
|
||||||
|
# Setup a funded user account on the main chain, and add an unfunded account.
|
||||||
|
# Setup address book and add a funded account on the mainchain.
|
||||||
|
# Typical female names are addresses on the mainchain.
|
||||||
|
# The first account is funded.
|
||||||
|
alice = mc_app.create_account('alice')
|
||||||
|
beth = mc_app.create_account('beth')
|
||||||
|
carol = mc_app.create_account('carol')
|
||||||
|
deb = mc_app.create_account('deb')
|
||||||
|
ella = mc_app.create_account('ella')
|
||||||
|
mc_app(Payment(account=params.genesis_account, dst=alice, amt=XRP(20_000)))
|
||||||
|
mc_app.maybe_ledger_accept()
|
||||||
|
|
||||||
|
# Typical male names are addresses on the sidechain.
|
||||||
|
# All accounts are initially unfunded
|
||||||
|
adam = sc_app.create_account('adam')
|
||||||
|
bob = sc_app.create_account('bob')
|
||||||
|
charlie = sc_app.create_account('charlie')
|
||||||
|
dan = sc_app.create_account('dan')
|
||||||
|
ed = sc_app.create_account('ed')
|
||||||
|
|
||||||
|
|
||||||
|
def run_all(mc_app: App, sc_app: App, params: Params):
|
||||||
|
setup_accounts(mc_app, sc_app, params)
|
||||||
|
logging.info(f'mainchain:\n{mc_app.key_manager.to_string()}')
|
||||||
|
logging.info(f'sidechain:\n{sc_app.key_manager.to_string()}')
|
||||||
|
simple_xrp_test(mc_app, sc_app, params)
|
||||||
|
simple_iou_test(mc_app, sc_app, params)
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple_xchain(configs_dirs_dict: Dict[int, str]):
|
||||||
|
tst_common.test_start(configs_dirs_dict, run_all)
|
||||||
74
bin/sidechain/python/tests/tst_common.py
Normal file
74
bin/sidechain/python/tests/tst_common.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import logging
|
||||||
|
import pprint
|
||||||
|
import pytest
|
||||||
|
from multiprocessing import Process, Value
|
||||||
|
from typing import Callable, Dict
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from app import App
|
||||||
|
from common import eprint, disable_eprint, XRP
|
||||||
|
from sidechain import Params
|
||||||
|
import sidechain
|
||||||
|
import test_utils
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def run(mc_app: App, sc_app: App, params: Params,
|
||||||
|
test_case: Callable[[App, App, Params], None]):
|
||||||
|
# process will run while stop token is non-zero
|
||||||
|
stop_token = Value('i', 1)
|
||||||
|
p = None
|
||||||
|
if mc_app.standalone:
|
||||||
|
p = Process(target=sidechain.close_mainchain_ledgers,
|
||||||
|
args=(stop_token, params))
|
||||||
|
p.start()
|
||||||
|
try:
|
||||||
|
test_case(mc_app, sc_app, params)
|
||||||
|
finally:
|
||||||
|
if p:
|
||||||
|
stop_token.value = 0
|
||||||
|
p.join()
|
||||||
|
sidechain._convert_log_files_to_json(
|
||||||
|
mc_app.get_configs() + sc_app.get_configs(), 'final.json')
|
||||||
|
|
||||||
|
|
||||||
|
def standalone_test(params: Params, test_case: Callable[[App, App, Params],
|
||||||
|
None]):
|
||||||
|
def callback(mc_app: App, sc_app: App):
|
||||||
|
run(mc_app, sc_app, params, test_case)
|
||||||
|
|
||||||
|
sidechain._standalone_with_callback(params,
|
||||||
|
callback,
|
||||||
|
setup_user_accounts=False)
|
||||||
|
|
||||||
|
|
||||||
|
def multinode_test(params: Params, test_case: Callable[[App, App, Params],
|
||||||
|
None]):
|
||||||
|
def callback(mc_app: App, sc_app: App):
|
||||||
|
run(mc_app, sc_app, params, test_case)
|
||||||
|
|
||||||
|
sidechain._multinode_with_callback(params,
|
||||||
|
callback,
|
||||||
|
setup_user_accounts=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_start(configs_dirs_dict: Dict[int, str],
|
||||||
|
test_case: Callable[[App, App, Params], None]):
|
||||||
|
params = sidechain.Params(configs_dir=configs_dirs_dict[1])
|
||||||
|
|
||||||
|
if err_str := params.check_error():
|
||||||
|
eprint(err_str)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if params.verbose:
|
||||||
|
print("eprint enabled")
|
||||||
|
else:
|
||||||
|
disable_eprint()
|
||||||
|
|
||||||
|
# Set to true to help debug tests
|
||||||
|
test_utils.test_context_verbose_logging = True
|
||||||
|
|
||||||
|
if params.standalone:
|
||||||
|
standalone_test(params, test_case)
|
||||||
|
else:
|
||||||
|
multinode_test(params, test_case)
|
||||||
366
bin/sidechain/python/transaction.py
Normal file
366
bin/sidechain/python/transaction.py
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
from typing import Dict, List, Optional, Union
|
||||||
|
|
||||||
|
from command import Command
|
||||||
|
from common import Account, Asset, Path, PathList, to_rippled_epoch
|
||||||
|
|
||||||
|
|
||||||
|
class Transaction(Command):
|
||||||
|
'''Interface for all transactions'''
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
account: Account,
|
||||||
|
flags: Optional[int] = None,
|
||||||
|
fee: Optional[Union[Asset, int]] = None,
|
||||||
|
sequence: Optional[int] = None,
|
||||||
|
account_txn_id: Optional[str] = None,
|
||||||
|
last_ledger_sequence: Optional[int] = None,
|
||||||
|
src_tag: Optional[int] = None,
|
||||||
|
memos: Optional[List[Dict[str, dict]]] = None,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.account = account
|
||||||
|
# set even if None
|
||||||
|
self.flags = flags
|
||||||
|
self.fee = fee
|
||||||
|
self.sequence = sequence
|
||||||
|
self.account_txn_id = account_txn_id
|
||||||
|
self.last_ledger_sequence = last_ledger_sequence
|
||||||
|
self.src_tag = src_tag
|
||||||
|
self.memos = memos
|
||||||
|
|
||||||
|
def cmd_name(self) -> str:
|
||||||
|
return 'submit'
|
||||||
|
|
||||||
|
def set_seq_and_fee(self, seq: int, fee: Union[Asset, int]):
|
||||||
|
self.sequence = seq
|
||||||
|
self.fee = fee
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
txn = {
|
||||||
|
'Account': self.account.account_id,
|
||||||
|
}
|
||||||
|
if self.flags is not None:
|
||||||
|
txn['Flags'] = flags
|
||||||
|
if self.fee is not None:
|
||||||
|
if isinstance(self.fee, int):
|
||||||
|
txn['Fee'] = f'{self.fee}' # must be a string
|
||||||
|
else:
|
||||||
|
txn['Fee'] = self.fee.to_cmd_obj()
|
||||||
|
if self.sequence is not None:
|
||||||
|
txn['Sequence'] = self.sequence
|
||||||
|
if self.account_txn_id is not None:
|
||||||
|
txn['AccountTxnID'] = self.account_txn_id
|
||||||
|
if self.last_ledger_sequence is not None:
|
||||||
|
txn['LastLedgerSequence'] = self.last_ledger_sequence
|
||||||
|
if self.src_tag is not None:
|
||||||
|
txn['SourceTag'] = self.src_tag
|
||||||
|
if self.memos is not None:
|
||||||
|
txn['Memos'] = self.memos
|
||||||
|
return txn
|
||||||
|
|
||||||
|
|
||||||
|
class Payment(Transaction):
|
||||||
|
'''A payment transaction'''
|
||||||
|
def __init__(self,
|
||||||
|
*,
|
||||||
|
dst: Account,
|
||||||
|
amt: Asset,
|
||||||
|
send_max: Optional[Asset] = None,
|
||||||
|
paths: Optional[PathList] = None,
|
||||||
|
dst_tag: Optional[int] = None,
|
||||||
|
deliver_min: Optional[Asset] = None,
|
||||||
|
**rest):
|
||||||
|
super().__init__(**rest)
|
||||||
|
self.dst = dst
|
||||||
|
self.amt = amt
|
||||||
|
self.send_max = send_max
|
||||||
|
if paths is not None and isinstance(paths, Path):
|
||||||
|
# allow paths = Path([...]) special case
|
||||||
|
self.paths = PathList([paths])
|
||||||
|
else:
|
||||||
|
self.paths = paths
|
||||||
|
self.dst_tag = dst_tag
|
||||||
|
self.deliver_min = deliver_min
|
||||||
|
|
||||||
|
def set_partial_payment(self, value: bool = True):
|
||||||
|
'''Set or clear the partial payment flag'''
|
||||||
|
self._set_flag(0x0002_0000, value)
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
'''convert to transaction form (suitable for using json.dumps or similar)'''
|
||||||
|
txn = super().to_cmd_obj()
|
||||||
|
txn = {
|
||||||
|
**txn,
|
||||||
|
'TransactionType': 'Payment',
|
||||||
|
'Destination': self.dst.account_id,
|
||||||
|
'Amount': self.amt.to_cmd_obj(),
|
||||||
|
}
|
||||||
|
if self.paths is not None:
|
||||||
|
txn['Paths'] = self.paths.to_cmd_obj()
|
||||||
|
if self.send_max is not None:
|
||||||
|
txn['SendMax'] = self.send_max.to_cmd_obj()
|
||||||
|
if self.dst_tag is not None:
|
||||||
|
txn['DestinationTag'] = self.dst_tag
|
||||||
|
if self.deliver_min is not None:
|
||||||
|
txn['DeliverMin'] = self.deliver_min
|
||||||
|
return txn
|
||||||
|
|
||||||
|
|
||||||
|
class Trust(Transaction):
|
||||||
|
'''A trust set transaction'''
|
||||||
|
def __init__(self,
|
||||||
|
*,
|
||||||
|
limit_amt: Optional[Asset] = None,
|
||||||
|
qin: Optional[int] = None,
|
||||||
|
qout: Optional[int] = None,
|
||||||
|
**rest):
|
||||||
|
super().__init__(**rest)
|
||||||
|
self.limit_amt = limit_amt
|
||||||
|
self.qin = qin
|
||||||
|
self.qout = qout
|
||||||
|
|
||||||
|
def set_auth(self):
|
||||||
|
'''Set the auth flag (cannot be cleared)'''
|
||||||
|
self._set_flag(0x00010000)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_no_ripple(self, value: bool = True):
|
||||||
|
'''Set or clear the noRipple flag'''
|
||||||
|
self._set_flag(0x0002_0000, value)
|
||||||
|
self._set_flag(0x0004_0000, not value)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_freeze(self, value: bool = True):
|
||||||
|
'''Set or clear the freeze flag'''
|
||||||
|
self._set_flag(0x0020_0000, value)
|
||||||
|
self._set_flag(0x0040_0000, not value)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
'''convert to transaction form (suitable for using json.dumps or similar)'''
|
||||||
|
result = super().to_cmd_obj()
|
||||||
|
result = {
|
||||||
|
**result,
|
||||||
|
'TransactionType': 'TrustSet',
|
||||||
|
'LimitAmount': self.limit_amt.to_cmd_obj(),
|
||||||
|
}
|
||||||
|
if self.qin is not None:
|
||||||
|
result['QualityIn'] = self.qin
|
||||||
|
if self.qout is not None:
|
||||||
|
result['QualityOut'] = self.qout
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class SetRegularKey(Transaction):
|
||||||
|
'''A SetRegularKey transaction'''
|
||||||
|
def __init__(self, *, key: str, **rest):
|
||||||
|
super().__init__(**rest)
|
||||||
|
self.key = key
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
'''convert to transaction form (suitable for using json.dumps or similar)'''
|
||||||
|
result = super().to_cmd_obj()
|
||||||
|
result = {
|
||||||
|
**result,
|
||||||
|
'TransactionType': 'SetRegularKey',
|
||||||
|
'RegularKey': self.key,
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class SignerListSet(Transaction):
|
||||||
|
'''A SignerListSet transaction'''
|
||||||
|
def __init__(self,
|
||||||
|
*,
|
||||||
|
keys: List[str],
|
||||||
|
weights: Optional[List[int]] = None,
|
||||||
|
quorum: int,
|
||||||
|
**rest):
|
||||||
|
super().__init__(**rest)
|
||||||
|
self.keys = keys
|
||||||
|
self.quorum = quorum
|
||||||
|
if weights:
|
||||||
|
if len(weights) != len(keys):
|
||||||
|
raise ValueError(
|
||||||
|
f'SignerSetList number of weights must equal number of keys (or be empty). Weights: {weights} Keys: {keys}'
|
||||||
|
)
|
||||||
|
self.weights = weights
|
||||||
|
else:
|
||||||
|
self.weights = [1] * len(keys)
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
'''convert to transaction form (suitable for using json.dumps or similar)'''
|
||||||
|
result = super().to_cmd_obj()
|
||||||
|
result = {
|
||||||
|
**result,
|
||||||
|
'TransactionType': 'SignerListSet',
|
||||||
|
'SignerQuorum': self.quorum,
|
||||||
|
}
|
||||||
|
entries = []
|
||||||
|
for k, w in zip(self.keys, self.weights):
|
||||||
|
entries.append({'SignerEntry': {'Account': k, 'SignerWeight': w}})
|
||||||
|
result['SignerEntries'] = entries
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class AccountSet(Transaction):
|
||||||
|
'''An account set transaction'''
|
||||||
|
def __init__(self, account: Account, **rest):
|
||||||
|
super().__init__(account=account, **rest)
|
||||||
|
self.clear_flag = None
|
||||||
|
self.set_flag = None
|
||||||
|
self.transfer_rate = None
|
||||||
|
self.tick_size = None
|
||||||
|
|
||||||
|
def _set_account_flag(self, flag_id: int, value):
|
||||||
|
if value:
|
||||||
|
self.set_flag = flag_id
|
||||||
|
else:
|
||||||
|
self.clear_flag = flag_id
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_account_txn_id(self, value: bool = True):
|
||||||
|
'''Set or clear the asfAccountTxnID flag'''
|
||||||
|
return self._set_account_flag(5, value)
|
||||||
|
|
||||||
|
def set_default_ripple(self, value: bool = True):
|
||||||
|
'''Set or clear the asfDefaultRipple flag'''
|
||||||
|
return self._set_account_flag(8, value)
|
||||||
|
|
||||||
|
def set_deposit_auth(self, value: bool = True):
|
||||||
|
'''Set or clear the asfDepositAuth flag'''
|
||||||
|
return self._set_account_flag(9, value)
|
||||||
|
|
||||||
|
def set_disable_master(self, value: bool = True):
|
||||||
|
'''Set or clear the asfDisableMaster flag'''
|
||||||
|
return self._set_account_flag(4, value)
|
||||||
|
|
||||||
|
def set_disallow_xrp(self, value: bool = True):
|
||||||
|
'''Set or clear the asfDisallowXRP flag'''
|
||||||
|
return self._set_account_flag(3, value)
|
||||||
|
|
||||||
|
def set_global_freeze(self, value: bool = True):
|
||||||
|
'''Set or clear the asfGlobalFreeze flag'''
|
||||||
|
return self._set_account_flag(7, value)
|
||||||
|
|
||||||
|
def set_no_freeze(self, value: bool = True):
|
||||||
|
'''Set or clear the asfNoFreeze flag'''
|
||||||
|
return self._set_account_flag(6, value)
|
||||||
|
|
||||||
|
def set_require_auth(self, value: bool = True):
|
||||||
|
'''Set or clear the asfRequireAuth flag'''
|
||||||
|
return self._set_account_flag(2, value)
|
||||||
|
|
||||||
|
def set_require_dest(self, value: bool = True):
|
||||||
|
'''Set or clear the asfRequireDest flag'''
|
||||||
|
return self._set_account_flag(1, value)
|
||||||
|
|
||||||
|
def set_transfer_rate(self, value: int):
|
||||||
|
'''Set the fee to change when users transfer this account's issued currencies'''
|
||||||
|
self.transfer_rate = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_tick_size(self, value: int):
|
||||||
|
'''Tick size to use for offers involving a currency issued by this address'''
|
||||||
|
self.tick_size = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
'''convert to transaction form (suitable for using json.dumps or similar)'''
|
||||||
|
result = super().to_cmd_obj()
|
||||||
|
result = {
|
||||||
|
**result,
|
||||||
|
'TransactionType': 'AccountSet',
|
||||||
|
}
|
||||||
|
if self.clear_flag is not None:
|
||||||
|
result['ClearFlag'] = self.clear_flag
|
||||||
|
if self.set_flag is not None:
|
||||||
|
result['SetFlag'] = self.set_flag
|
||||||
|
if self.transfer_rate is not None:
|
||||||
|
result['TransferRate'] = self.transfer_rate
|
||||||
|
if self.tick_size is not None:
|
||||||
|
result['TickSize'] = self.tick_size
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class Offer(Transaction):
|
||||||
|
'''An offer transaction'''
|
||||||
|
def __init__(self,
|
||||||
|
*,
|
||||||
|
taker_pays: Asset,
|
||||||
|
taker_gets: Asset,
|
||||||
|
expiration: Optional[int] = None,
|
||||||
|
offer_sequence: Optional[int] = None,
|
||||||
|
**rest):
|
||||||
|
super().__init__(**rest)
|
||||||
|
self.taker_pays = taker_pays
|
||||||
|
self.taker_gets = taker_gets
|
||||||
|
self.expiration = expiration
|
||||||
|
self.offer_sequence = offer_sequence
|
||||||
|
|
||||||
|
def set_passive(self, value: bool = True):
|
||||||
|
return self._set_flag(0x0001_0000, value)
|
||||||
|
|
||||||
|
def set_immediate_or_cancel(self, value: bool = True):
|
||||||
|
return self._set_flag(0x0002_0000, value)
|
||||||
|
|
||||||
|
def set_fill_or_kill(self, value: bool = True):
|
||||||
|
return self._set_flag(0x0004_0000, value)
|
||||||
|
|
||||||
|
def set_sell(self, value: bool = True):
|
||||||
|
return self._set_flag(0x0008_0000, value)
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
txn = super().to_cmd_obj()
|
||||||
|
txn = {
|
||||||
|
**txn,
|
||||||
|
'TransactionType': 'OfferCreate',
|
||||||
|
'TakerPays': self.taker_pays.to_cmd_obj(),
|
||||||
|
'TakerGets': self.taker_gets.to_cmd_obj(),
|
||||||
|
}
|
||||||
|
if self.expiration is not None:
|
||||||
|
txn['Expiration'] = self.expiration
|
||||||
|
if self.offer_sequence is not None:
|
||||||
|
txn['OfferSequence'] = self.offer_sequence
|
||||||
|
return txn
|
||||||
|
|
||||||
|
|
||||||
|
class Ticket(Transaction):
|
||||||
|
'''A ticket create transaction'''
|
||||||
|
def __init__(self, *, count: int = 1, **rest):
|
||||||
|
super().__init__(**rest)
|
||||||
|
self.count = count
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
txn = super().to_cmd_obj()
|
||||||
|
txn = {
|
||||||
|
**txn,
|
||||||
|
'TransactionType': 'TicketCreate',
|
||||||
|
'TicketCount': self.count,
|
||||||
|
}
|
||||||
|
return txn
|
||||||
|
|
||||||
|
|
||||||
|
class SetHook(Transaction):
|
||||||
|
'''A SetHook transaction for the experimental hook amendment'''
|
||||||
|
def __init__(self,
|
||||||
|
*,
|
||||||
|
create_code: str,
|
||||||
|
hook_on: str = '0000000000000000',
|
||||||
|
**rest):
|
||||||
|
super().__init__(**rest)
|
||||||
|
self.create_code = create_code
|
||||||
|
self.hook_on = hook_on
|
||||||
|
|
||||||
|
def to_cmd_obj(self) -> dict:
|
||||||
|
txn = super().to_cmd_obj()
|
||||||
|
txn = {
|
||||||
|
**txn,
|
||||||
|
'TransactionType': 'SetHook',
|
||||||
|
'CreateCode': self.create_code,
|
||||||
|
'HookOn': self.hook_on,
|
||||||
|
}
|
||||||
|
return txn
|
||||||
217
docs/sidechain/GettingStarted.md
Normal file
217
docs/sidechain/GettingStarted.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
## Introduction
|
||||||
|
|
||||||
|
This document walks through the steps to setup a side chain running on your local
|
||||||
|
machine and make your first cross chain transfers.
|
||||||
|
|
||||||
|
## Get Ready
|
||||||
|
|
||||||
|
This section describes how to install the python dependencies, create the
|
||||||
|
environment variables, and create the configuration files that scripts need to
|
||||||
|
run correctly.
|
||||||
|
|
||||||
|
### Build rippled
|
||||||
|
|
||||||
|
Checkout the `sidechain` branch from the rippled repository, and follow the
|
||||||
|
usual process to build rippled.
|
||||||
|
|
||||||
|
### Create a Python virtual environment and install dependencies
|
||||||
|
|
||||||
|
1. Check the current python version. The python scripts require python 3.8 or greater:
|
||||||
|
```
|
||||||
|
python3 --version
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Choose a directory to put the virtual environment. For example, `~/envs`.
|
||||||
|
|
||||||
|
3. Create this directory and cd to it:
|
||||||
|
```
|
||||||
|
$ mkdir ~/env
|
||||||
|
$ cd ~/env
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Create a new python virtual environment and activate it. Here the new
|
||||||
|
envrionment is called `sidechain`. Of course, you can choose whatever name
|
||||||
|
you'd like:
|
||||||
|
```
|
||||||
|
$ python3 -m venv sidechain
|
||||||
|
$ source ./sidechain/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Install the required python modules. Change directories to where the
|
||||||
|
side chain branch is located and use pip3 to install the modules: Assuming the
|
||||||
|
code is located in `~/projs/sidechain`, the following commands will do it:
|
||||||
|
```
|
||||||
|
cd ~/projs/sidechain
|
||||||
|
$ pip3 install -r bin/sidechain/python/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Activate the python virtual environment
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cd ~/env
|
||||||
|
$ source ./sidechain/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
There's no harm if it was already active.
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
The python scripts need to know the locations of two files and one directory.
|
||||||
|
These can be specified either through command line arguments or by setting
|
||||||
|
environment variables.
|
||||||
|
|
||||||
|
1. The location of the rippled executable used for main chain servers. Either
|
||||||
|
set the environment variable `RIPPLED_MAINCHAIN_EXE` or use the command line
|
||||||
|
switch `--exe_mainchain`. Until a new RPC is integrated into the main branch
|
||||||
|
(this will happen very soon), use the code built from the sidechain branch as
|
||||||
|
the main chain exe.
|
||||||
|
2. The location of the rippled executable used for side chain servers. Either
|
||||||
|
set the environment variable `RIPPLED_SIDECHAIN_EXE` or use the command line
|
||||||
|
switch `--exe_sidechain`. This should be the rippled executable built from
|
||||||
|
the sidechain branch.
|
||||||
|
3. The location of the directory that has the rippled configuration files.
|
||||||
|
Either set the environment variable `RIPPLED_SIDECHAIN_CFG_DIR` or use the
|
||||||
|
command line switch `--cfgs_dir`. The configuration files do not exist yet.
|
||||||
|
There is a script to create these for you. For now, just choose a location
|
||||||
|
where the files should live and make sure that directory exists.
|
||||||
|
|
||||||
|
Setting environment variables can be very convient. For example, a small script
|
||||||
|
can be sourced to set these environment variables when working with side chains.
|
||||||
|
|
||||||
|
|
||||||
|
### Creating configuration files
|
||||||
|
|
||||||
|
Assuming rippled is built, the three environment variables are set, and the
|
||||||
|
python environment is activated, run the following script:
|
||||||
|
```
|
||||||
|
bin/sidechain/python/create_config_files.py --usd
|
||||||
|
```
|
||||||
|
|
||||||
|
There should now be many configuration files in the directory specified by the
|
||||||
|
`RIPPLED_SIDECHAIN_CFG_DIR` environment variable. The `--usd` creates a sample
|
||||||
|
cross chain assert for USD -> USD transfers.
|
||||||
|
|
||||||
|
## Running the interactive shell
|
||||||
|
|
||||||
|
There is an interactive shell called `RiplRepl` that can be used to explore
|
||||||
|
side chains. It will use the configuration files built above to spin up test
|
||||||
|
rippled main chain running in standalone mode as well as 5 side chain federators
|
||||||
|
running in regular consensus mode.
|
||||||
|
|
||||||
|
To start the shell, run the following script:
|
||||||
|
```
|
||||||
|
bin/sidechain/python/riplrepl.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The shell will not start until the servers have synced. It may take a minute or
|
||||||
|
two until they do sync. The script should give feedback while it is syncing.
|
||||||
|
|
||||||
|
Once the shell has started, the following message should appear:
|
||||||
|
```
|
||||||
|
Welcome to the sidechain test shell. Type help or ? to list commands.
|
||||||
|
|
||||||
|
RiplRepl>
|
||||||
|
```
|
||||||
|
|
||||||
|
Type the command `server_info` to make sure the servers are running. An example output would be:
|
||||||
|
```
|
||||||
|
RiplRepl> server_info
|
||||||
|
pid config running server_state ledger_seq complete_ledgers
|
||||||
|
main 0 136206 main.no_shards.mainchain_0/rippled.cfg True proposing 75 2-75
|
||||||
|
side 0 136230 sidechain_0/rippled.cfg True proposing 92 1-92
|
||||||
|
1 136231 sidechain_1/rippled.cfg True proposing 92 1-92
|
||||||
|
2 136232 sidechain_2/rippled.cfg True proposing 92 1-92
|
||||||
|
3 136233 sidechain_3/rippled.cfg True proposing 92 1-92
|
||||||
|
4 136234 sidechain_4/rippled.cfg True proposing 92 1-92
|
||||||
|
```
|
||||||
|
|
||||||
|
Of course, you'll see slightly different output on your machine. The important
|
||||||
|
thing to notice is there are two categories, one called `main` for the main chain
|
||||||
|
and one called `side` for the side chain. There should be a single server for the
|
||||||
|
main chain and five servers for the side chain.
|
||||||
|
|
||||||
|
Next, type the `balance` command, to see the balances of the accounts in the address book:
|
||||||
|
```
|
||||||
|
RiplRepl> balance
|
||||||
|
balance currency peer limit
|
||||||
|
account
|
||||||
|
main root 99,999,989,999.999985 XRP
|
||||||
|
door 9,999.999940 XRP
|
||||||
|
side door 99,999,999,999.999954 XRP
|
||||||
|
```
|
||||||
|
|
||||||
|
There are two accounts on the main chain: `root` and `door`; and one account on the side chain: `door`. These are not user accounts. Let's add two user accounts, one on the main chain called `alice` and one on the side chain called `bob`. The `new_account` command does this for us.
|
||||||
|
|
||||||
|
```
|
||||||
|
RiplRepl> new_account mainchain alice
|
||||||
|
RiplRepl> new_account sidechain bob
|
||||||
|
```
|
||||||
|
|
||||||
|
This just added the accounts to the address book, but they don't exist on the
|
||||||
|
ledger yet. To do that, we need to fund the accounts with a payment. For now,
|
||||||
|
let's just fund the `alice` account and check the balances. The `pay` command
|
||||||
|
makes a payment on one of the chains:
|
||||||
|
|
||||||
|
```
|
||||||
|
RiplRepl> pay mainchain root alice 5000
|
||||||
|
RiplRepl> balance
|
||||||
|
balance currency peer limit
|
||||||
|
account
|
||||||
|
main root 99,999,984,999.999969 XRP
|
||||||
|
door 9,999.999940 XRP
|
||||||
|
alice 5,000.000000 XRP
|
||||||
|
side door 99,999,999,999.999954 XRP
|
||||||
|
bob 0.000000 XRP
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, let's do something specific to side chains: make a cross chain payment.
|
||||||
|
The `xchain` command makes a payment between chains:
|
||||||
|
|
||||||
|
```
|
||||||
|
RiplRepl> xchain mainchain alice bob 4000
|
||||||
|
RiplRepl> balance
|
||||||
|
balance currency peer limit
|
||||||
|
account
|
||||||
|
main root 99,999,984,999.999969 XRP
|
||||||
|
door 13,999.999940 XRP
|
||||||
|
alice 999.999990 XRP
|
||||||
|
side door 99,999,995,999.999863 XRP
|
||||||
|
bob 4,000.000000 XRP
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: the account reserve on the side chain is 100 XRP. The cross chain amount
|
||||||
|
must be greater than 100 XRP or the payment will fail.
|
||||||
|
|
||||||
|
Making a cross chain transaction from the side chain to the main chain is similar:
|
||||||
|
```
|
||||||
|
RiplRepl> xchain sidechain bob alice 2000
|
||||||
|
RiplRepl> balance
|
||||||
|
balance currency peer limit
|
||||||
|
account
|
||||||
|
main root 99,999,984,999.999969 XRP
|
||||||
|
door 11,999.999840 XRP
|
||||||
|
alice 2,999.999990 XRP
|
||||||
|
side door 99,999,997,999.999863 XRP
|
||||||
|
bob 1,999.999990 XRP
|
||||||
|
```
|
||||||
|
|
||||||
|
If you typed `balance` very quickly, you may catch a cross chain payment in
|
||||||
|
progress and the XRP may be deducted from bob's account before it is added to
|
||||||
|
alice's. If this happens just wait a couple seconds and retry the command. Also
|
||||||
|
note that accounts pay a ten drop fee when submitting transactions.
|
||||||
|
|
||||||
|
Finally, exit the program with the `quit` command:
|
||||||
|
```
|
||||||
|
RiplRepl> quit
|
||||||
|
Thank you for using RiplRepl. Goodbye.
|
||||||
|
|
||||||
|
|
||||||
|
WARNING: Server 0 is being stopped. RPC commands cannot be sent until this is restarted.
|
||||||
|
```
|
||||||
|
|
||||||
|
Ignore the warning about the server being stopped.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Those two cross chain payments are a "hello world" for side chains. It makes sure
|
||||||
|
you're environment is set up correctly.
|
||||||
130
docs/sidechain/configFile.md
Normal file
130
docs/sidechain/configFile.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
## Introduction
|
||||||
|
|
||||||
|
The config file for side chain servers that run as federators require three
|
||||||
|
addition configuration stanzas. One additional stanza is required if the
|
||||||
|
federator will run in standalone mode, and one existing stanza (`ips_fixed`) can
|
||||||
|
be useful if running a side chain network on the local machine.
|
||||||
|
|
||||||
|
## The `[sidechain]` stanza
|
||||||
|
|
||||||
|
This stanza defines the side chain top level parameters. This includes:
|
||||||
|
* The federator's signing key. This is needed to add a signature to a
|
||||||
|
mutli-signed transaction before submitting it on the main chain or the side
|
||||||
|
chain.
|
||||||
|
* The main chain account. This is the account controlled by the federators and
|
||||||
|
the account users will send their assets to initiate cross chain transactions.
|
||||||
|
Some documentation calls this the main chain "door" account.
|
||||||
|
* The ip address and port of the main chain. This is needed to communicate with
|
||||||
|
the main chain server.
|
||||||
|
|
||||||
|
An example stanza may look like this (where the "X" are part of a secret key):
|
||||||
|
```
|
||||||
|
[sidechain]
|
||||||
|
signing_key=sXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
mainchain_account=rDj4pMuPv8gAD5ZvUrpHza3bn6QMAK6Zoo
|
||||||
|
mainchain_ip=127.0.0.1
|
||||||
|
mainchain_port_ws=6007
|
||||||
|
```
|
||||||
|
|
||||||
|
## The `[sidechain_federators]` stanza
|
||||||
|
|
||||||
|
This stanza defines the signing public keys of the sidechain federators. This is
|
||||||
|
needed to know which servers to collect transaction signatures from. An example
|
||||||
|
stanza may look like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
[sidechain_federators]
|
||||||
|
aKNmFC2QWXbCUFq9XxaLgz1Av6SY5ccE457zFjSoNwaFPGEwz6ab
|
||||||
|
aKE9m7iDjhy5QAtnrmE8RVbY4RRvFY1Fn3AZ5NN2sB4N9EzQe82Z
|
||||||
|
aKNFZ3L7Y7z8SdGVewkVuqMKmDr6bqmaErXBdWAVqv1cjgkt1X36
|
||||||
|
aKEhTF5hRYDenn2Rb1NMza1vF9RswX8gxyJuuYmz6kpU5W6hc7zi
|
||||||
|
aKEydZ5rmPm7oYQZi9uagk8fnbXz4gmx82WBTJcTVdgYWfRBo1Mf
|
||||||
|
```
|
||||||
|
|
||||||
|
## The `[sidechain_assets]` and associated stanzas.
|
||||||
|
|
||||||
|
These stanza define what asset is used as the cross chain asset between the main
|
||||||
|
chain and the side chain. The `mainchain_asset` is the asset that accounts on
|
||||||
|
the main chain send to the account controlled by the federators to initiate an
|
||||||
|
cross chain transaction. The `sidechain_asset` is the asset that will be sent to
|
||||||
|
the destination address on the side chain. When returning an asset from the side
|
||||||
|
chain to the main chain, the `sidechain_asset` is sent to the side chain account
|
||||||
|
controlled by the federators and the `mainchain_asset` will be sent to the
|
||||||
|
destination address on the main chain. There are amounts associated with these
|
||||||
|
two assets. These amount define an exchange rate. If the value of the main chain
|
||||||
|
asset is 1, and the amount of the side chain asset is 2, then for every asset
|
||||||
|
locked on the main chain, twice of many assets are sent on the side chain.
|
||||||
|
Similarly, for every asset returned from the main chain, half as many assets are
|
||||||
|
sent on the main chain. The format used to specify these amounts is the same as
|
||||||
|
used in json RPC commands.
|
||||||
|
|
||||||
|
There are also fields for "refund_penalty" on the main chain and side chain.
|
||||||
|
This is the amount to deduct from refunds if a transaction fails. For example,
|
||||||
|
if a cross chain transaction sends 1 XRP to an address on the side chain that
|
||||||
|
doesn't exist (and the reserve is greater than 1 XRP), then a refund is issued
|
||||||
|
on the main chain. If the `mainchain_refund_penalty` is 400 drops, then the
|
||||||
|
amount returned is 1 XRP - 400 drops.
|
||||||
|
|
||||||
|
An example of stanzas where the main chain asset is XRP, and the sidechain asset
|
||||||
|
is also XRP, and the exchange rate is 1 to 1 may look like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
[sidechain_assets]
|
||||||
|
xrp_xrp_sidechain_asset
|
||||||
|
|
||||||
|
[xrp_xrp_sidechain_asset]
|
||||||
|
mainchain_asset="1"
|
||||||
|
sidechain_asset="1"
|
||||||
|
mainchain_refund_penalty="400"
|
||||||
|
sidechain_refund_penalty="400"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
An example of stanzas where the main chain asset is USD/rD... and the side chain
|
||||||
|
asset is USD/rHb... and the exchange rate is 1 to 2 may look like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
[sidechain_assets]
|
||||||
|
iou_iou_sidechain_asset
|
||||||
|
|
||||||
|
[iou_iou_sidechain_asset]
|
||||||
|
mainchain_asset={"currency": "USD", "issuer": "rDj4pMuPv8gAD5ZvUrpHza3bn6QMAK6Zoo", "value": "1"}
|
||||||
|
sidechain_asset={"currency": "USD", "issuer": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "value": "2"}
|
||||||
|
mainchain_refund_penalty={"currency": "USD", "issuer": "rDj4pMuPv8gAD5ZvUrpHza3bn6QMAK6Zoo", "value": "0.02"}
|
||||||
|
sidechain_refund_penalty={"currency": "USD", "issuer": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "value": "0.04"}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## The `[sidechain_federators_secrets]` stanza
|
||||||
|
|
||||||
|
When running a side chain with a single federator in stand alone mode (useful
|
||||||
|
for debugging), that single server needs to know the signing keys of all the
|
||||||
|
federators in order to submit transactions. This stanza will not normally only
|
||||||
|
be part of configuration files that are used for testing and debugging.
|
||||||
|
|
||||||
|
An example of a stanza with federator secrets may look like this (where the "X"
|
||||||
|
are part of a secret key).
|
||||||
|
|
||||||
|
```
|
||||||
|
[sidechain_federators_secrets]
|
||||||
|
sXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
sXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
sXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
sXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
sXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
```
|
||||||
|
|
||||||
|
## The `[ips_fixed]` stanza
|
||||||
|
|
||||||
|
When running a test net it can be useful to hard code the ip addresses of the
|
||||||
|
side chain servers. An example of such a stanza used to run a test net locally
|
||||||
|
may look like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ips_fixed]
|
||||||
|
127.0.0.2 51238
|
||||||
|
127.0.0.3 51239
|
||||||
|
127.0.0.4 51240
|
||||||
|
127.0.0.5 51241
|
||||||
|
```
|
||||||
823
docs/sidechain/design.md
Normal file
823
docs/sidechain/design.md
Normal file
@@ -0,0 +1,823 @@
|
|||||||
|
# Introduction
|
||||||
|
|
||||||
|
This document covers the design of side chains using the XRP ledger. It covers
|
||||||
|
the implementation of federators, how federators are initially synced and kept
|
||||||
|
in sync, how cross chain transactions work, and how errors are handled. It does
|
||||||
|
not give a high level overview of side chains or describe their benefits.
|
||||||
|
|
||||||
|
# Terminology
|
||||||
|
|
||||||
|
_federator_: A server that listens for triggering transactions on both the main
|
||||||
|
chain and the side chain. Each federator has a signing key associated with it
|
||||||
|
that is used to sign transactions. A transaction must be signed by a quorum of
|
||||||
|
federators before it can be submitted. Federators are responsible for creating
|
||||||
|
and signing valid response transactions, collecting signatures from other
|
||||||
|
federators, and submitting transactions to the main chain and side chain.
|
||||||
|
|
||||||
|
_main chain_: Ledger where assets originate and where assets will be locked
|
||||||
|
while used on the side chain. For most applications, the main chain will be the
|
||||||
|
XRP ledger mainnet.
|
||||||
|
|
||||||
|
_side chain_: Ledger where proxy assets for the locked main chain assets are
|
||||||
|
issued. Side chains may have rules, transactors, and validators that are very
|
||||||
|
different from the main chain. Proxy assets on the side chain can be sent back
|
||||||
|
to the main chain where they will be be unlocked from the control of the
|
||||||
|
federators.
|
||||||
|
|
||||||
|
_door account_: Account controlled by the federators. There are two door
|
||||||
|
accounts: one on the main chain and one on the side chain. Cross chain
|
||||||
|
transactions are started by users sending assets to a door account. Main chain
|
||||||
|
to side chain transactions cause the balance to increase on the main chain door
|
||||||
|
account and the balance to decrease on the side chain door account. It is called
|
||||||
|
a "door" because it is the mechanism to move assets from one chain to another -
|
||||||
|
much like going between rooms in a house requires stepping through a door.
|
||||||
|
|
||||||
|
_triggering transaction_: A transaction that causes the federators to start the
|
||||||
|
process of signing and submitting a new response transaction. For example,
|
||||||
|
sending XRP to the main chain's door account is a triggering transaction that
|
||||||
|
will cause the federators to submit a new transaction on the side chain.
|
||||||
|
|
||||||
|
_response transaction_: A transaction submitted by the federators in reaction to
|
||||||
|
a triggering transaction. Note that _triggering transaction_ and _response
|
||||||
|
transaction_ depends on context. Sending XRP from a _door account_ to a user
|
||||||
|
account is a _response transaction_ when thinking about cross chain
|
||||||
|
transactions. It is a _triggering transaction_ when thinking about how to handle
|
||||||
|
failed transactions.
|
||||||
|
|
||||||
|
# New RPC Command is a key primitive
|
||||||
|
|
||||||
|
Side chains introduce a new subscription stream called
|
||||||
|
"account_history_tx_stream". Given an account, this streams both new
|
||||||
|
transactions and historical transactions from validated ledgers back to the
|
||||||
|
client. The transactions are streamed in order and without gaps, and each
|
||||||
|
transaction is given a numeric id. New transactions start at id 0 and continue
|
||||||
|
in the positive direction. Historical transaction start at id -1 and continue in
|
||||||
|
the negative direction. New transactions are sent in the same order as they were
|
||||||
|
applied to the ledger, and historical tranasations are in the reverse order they
|
||||||
|
were applied to the ledger. The server will continue to stream historical
|
||||||
|
transaction until it reaches the account's first transaction or the user sends a
|
||||||
|
command signaling that historical transactions are no longer needed. This can be
|
||||||
|
done without closing the stream, and new transactions will continue to be sent.
|
||||||
|
Note that these transactions include all the transactions that effect the
|
||||||
|
account, not just triggering and response transactions.
|
||||||
|
|
||||||
|
It's important to note that while historical and new transactions may be
|
||||||
|
interleaved in the stream, there are never any gaps in the transactions.
|
||||||
|
Transaction 7 MUST be sent before transaction 8, and transaction -7 MUST be sent
|
||||||
|
before transaction -8.
|
||||||
|
|
||||||
|
This is the key primitive that allows federators to agree on transaction
|
||||||
|
values - transaction types, sequence numbers, asset amounts, and destination
|
||||||
|
addresses - without communicating amung themselves (of course signing
|
||||||
|
transaction requires communication). Since the transactions are from validated
|
||||||
|
ledgers, all the federators will see the same transactions in the same order.
|
||||||
|
|
||||||
|
# Federators
|
||||||
|
|
||||||
|
## Federator Introduction
|
||||||
|
|
||||||
|
A federator acts as a bridge between a main chain and a side chain. Through a
|
||||||
|
multi-signature scheme, the federators collectively control an account on the
|
||||||
|
main chain and an account on the side chain. These accounts are called door
|
||||||
|
accounts. A federator listens for transactions on these door accounts. When a
|
||||||
|
federator hears a triggering transaction, it will eventually submit a new
|
||||||
|
response transaction that completes the triggering transaction.
|
||||||
|
|
||||||
|
Initially, the federators will live in the same executable as the side chain
|
||||||
|
validators. However, the proposed implementation purposely does not take
|
||||||
|
advantage of this fact. The motivation for this is:
|
||||||
|
|
||||||
|
1. It makes it easy to eventually separate out the federator implementation from
|
||||||
|
side chain validators.
|
||||||
|
2. The side chain to main chain transactions will be implemented the same way as
|
||||||
|
the main chain to side chain transactions. Building and maintaining one
|
||||||
|
implementation is preferable to maintaining two implementations.
|
||||||
|
|
||||||
|
## Keeping the federators in sync
|
||||||
|
|
||||||
|
Federators decide to sign transactions by using the "account_history_tx_stream"
|
||||||
|
to listen for transactions on each chain. New transactions on the main chain
|
||||||
|
will cause a federator to sign a transaction meant for the side chain.
|
||||||
|
Similarly, new transactions on the side chain will cause a federator to sign a
|
||||||
|
transaction meant for the main chain. As a concrete example, consider how XRP is
|
||||||
|
locked on the main chain and distributed on a side chain. A user sends XRP to
|
||||||
|
the main chain door account. This causes the federators to submit a transaction
|
||||||
|
sending XRP to a destination on the side chain. Recall that a transaction that
|
||||||
|
causes a federator to sign a transaction is called a triggering transaction, and
|
||||||
|
a transaction created to handle a triggering transaction is called a response
|
||||||
|
transaction. In the example above, the user sending XRP on the main chain is a
|
||||||
|
triggering transaction and the tranasaction created by the federators and
|
||||||
|
submitted on the side chain is a response transaction.
|
||||||
|
|
||||||
|
When a new triggering transaction is detected, a federator needs to create a
|
||||||
|
response transaction. The fee, destination address, and amount are all known.
|
||||||
|
The only value that isn't fixed or derived from the triggering transaction is
|
||||||
|
the account sequence number. It is easy to agree on a sequence number. But first
|
||||||
|
let's show that stream of triggering transactions are the same for all the
|
||||||
|
validators.
|
||||||
|
|
||||||
|
Notice that a response transaction is always on the opposite chain as the
|
||||||
|
corresponding triggering transaction. If a response transaction could be on
|
||||||
|
either chain, then there would be timing issues. Consider what would happen if
|
||||||
|
two triggering transaction came in, one from the main chain and one from the
|
||||||
|
side chain, and both of these triggering transactions required response
|
||||||
|
transactions on the main chain. Since there are separate transaction streams
|
||||||
|
comming from the main chain and side chain, different federators may see these
|
||||||
|
transaction arrive in different orders. However, since triggering and response
|
||||||
|
transactions are on different chains, the federators don't need to deal with
|
||||||
|
this case. (Note: there is at one response transaction that is needed in
|
||||||
|
exceptional circumstances that violates this. Tickets are used to handle this
|
||||||
|
transactions and will be described later).
|
||||||
|
|
||||||
|
Also notice that "account_history_tx_stream" delivers transactions in the order
|
||||||
|
they were applied to the ledger without gaps.
|
||||||
|
|
||||||
|
This means that once a federator is in a state where it knows what sequence
|
||||||
|
number should be used for the next response transaction (call it S), it will
|
||||||
|
know the sequence number for all the subsequent response transactions. It will
|
||||||
|
just be S+1, S+2, S+3, ect. Also notice that once in sync all the federators
|
||||||
|
will see the same transactions in the same order. So all the federators will
|
||||||
|
create the same response transactions and in the same order.
|
||||||
|
|
||||||
|
## Getting a federator in sync
|
||||||
|
|
||||||
|
When a federator joins the network, it doesn't know which triggering
|
||||||
|
transactions it needs to sign and it doesn't know what sequence numbers should
|
||||||
|
be used for response transactions. The "account_history_tx_stream" can be used
|
||||||
|
to find this information and get the federator to a state where it can start
|
||||||
|
signing and submitting transactions.
|
||||||
|
|
||||||
|
While getting into sync, a federator collects historical and new transactions
|
||||||
|
from both the side chain and main chain. Each transaction has an id that is used
|
||||||
|
to keep the transactions in the same order they were applied to the ledger, and
|
||||||
|
the collections are kept in this order.
|
||||||
|
|
||||||
|
One piece of information a syncing federator needs to find is which triggering
|
||||||
|
transactions need to be treated as new and which ones have already been handled.
|
||||||
|
This is easily found by looking at the historical transaction from the
|
||||||
|
"account_history_tx_stream". The first time a response transaction is found,
|
||||||
|
the hash of associated triggering transaction is noted (this is be recorded as
|
||||||
|
a memo in the response transaction). All the triggering transactions that
|
||||||
|
precede the noted triggering transaction have already been handled. The
|
||||||
|
"account_history_tx_stream" must continue to stream historical transactions at
|
||||||
|
least until the first response transaction is found. For example, if the first
|
||||||
|
observed response transaction on the main chain has hash `r_hash_mainchain`
|
||||||
|
and associated triggering transaction of `t_hash_sidechain`, that means we know
|
||||||
|
all the triggering transactions on the side chain before `t_hash_sidechain` have
|
||||||
|
been handled (including `t_hash_sidechain`).
|
||||||
|
|
||||||
|
Another set of data that needs to be collected are all the historical
|
||||||
|
triggering transactions that come after `t_hash` (see above). Of course,
|
||||||
|
`t_hash` comes from the "account_history_tx_stream" from one chain, and the
|
||||||
|
triggering transactions come from the other chain. This means more
|
||||||
|
transactions than needed may be gathered.
|
||||||
|
|
||||||
|
Historical transaction continue to be streamed until the triggering transaction
|
||||||
|
associated with `t_hash_this_chain` is found and value of `t_hash_other_chain`
|
||||||
|
is found. For example, the main chain will continue to collect historical
|
||||||
|
transaction until:
|
||||||
|
|
||||||
|
1) The side chain stream has found a response transaction and informed the main
|
||||||
|
chain of the hash of the associated triggering transaction.
|
||||||
|
|
||||||
|
2) The side chain stream has found that triggering transaction.
|
||||||
|
|
||||||
|
3) This main chain stream has found a response transaction and informed the side
|
||||||
|
chain syncing algorithm of the associated triggering transaction.
|
||||||
|
|
||||||
|
The above description does not handle the case where the start of histoical
|
||||||
|
transactions is reached without finding any response transactions. If this
|
||||||
|
happens then the other chain must also collect all the historical transactions,
|
||||||
|
since we cannot show that triggering transaction has ever been handled.
|
||||||
|
|
||||||
|
Once this data has been collected, a command will be sent to ask the
|
||||||
|
"account_history_tx_stream" to stop sending historical transaction (Note:
|
||||||
|
without closing the stream. If the stream were closed it would be possible to
|
||||||
|
miss a transaction). Starting from the transaction after the `t_hash`
|
||||||
|
transaction, the collected triggering transaction will be iterated in the order
|
||||||
|
they were applied to the ledger and treated as if they were newly arrived from
|
||||||
|
the transaction stream. Once this is done the federator is synced and can switch
|
||||||
|
to handling new transactions normally.
|
||||||
|
|
||||||
|
As long as there are regular cross chain transactions being sent from both the
|
||||||
|
main chain and the side chain, the above procedure doesn't require too many
|
||||||
|
historical transactions. However, if one chain almost never sends cross chain
|
||||||
|
transactions then the syncing procedure is not as effiecient as it could be. As
|
||||||
|
an extreme example, consider a main chain that sends cross chain transactions to
|
||||||
|
a side chain, and the side chain never sends cross chain transactions back.
|
||||||
|
Since there would be no response transactions on the main chain, the sync
|
||||||
|
algorithm would fetch all of the main chain transactions. One way to improve
|
||||||
|
this situation is for the federators to checkpoint the last known response
|
||||||
|
transaction and its corresponding triggering transactions. If they did this
|
||||||
|
individually, then on startup a federator would need to fetch at most as much
|
||||||
|
history as the time it was down. If they did this as a group (by adding a new
|
||||||
|
ledger object on the side chain, for example), then syncing could require much
|
||||||
|
less history. For now, these strategies are not used. The benefits of a simplier
|
||||||
|
implementations and not adding any new ledger objects outweighted the benefits
|
||||||
|
of faster syncing for some types of sidechains.
|
||||||
|
|
||||||
|
## Federator Implementation
|
||||||
|
|
||||||
|
The Federator is an event loop that services events sent to it from the
|
||||||
|
listeners. Events are handed in the `mainLoop` method. This runs on a separate
|
||||||
|
thread. It runs on a separate thread so all the event handlers run on the order
|
||||||
|
they were received on and on the same thread.
|
||||||
|
|
||||||
|
### Federator Events
|
||||||
|
|
||||||
|
A `Federator` event is a `std::variant` of all the event types. The current
|
||||||
|
event types are:
|
||||||
|
|
||||||
|
* `XChainTransferDetected`. This is added when a federator detects the start of
|
||||||
|
cross chain transaction.
|
||||||
|
```c++
|
||||||
|
struct XChainTransferDetected
|
||||||
|
{
|
||||||
|
// direction of the transfer
|
||||||
|
Dir dir_;
|
||||||
|
// Src account on the src chain
|
||||||
|
AccountID src_;
|
||||||
|
// Dst account on the dst chain
|
||||||
|
AccountID dst_;
|
||||||
|
STAmount deliveredAmt_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
uint256 txnHash_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
* `XChainTransferResult`. This is added when a federator detects the end of a
|
||||||
|
cross chain transaction.
|
||||||
|
```c++
|
||||||
|
struct XChainTransferResult
|
||||||
|
{
|
||||||
|
// direction is the direction of the triggering transaction.
|
||||||
|
// I.e. A "mainToSide" transfer result is a transaction that
|
||||||
|
// happens on the sidechain (the triggering transaction happended on the
|
||||||
|
// mainchain)
|
||||||
|
Dir dir_;
|
||||||
|
AccountID dst_;
|
||||||
|
std::optional<STAmount> deliveredAmt_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
// Txn hash of the initiating xchain transaction
|
||||||
|
uint256 srcChainTxnHash_;
|
||||||
|
// Txn has of the federator's transaction on the dst chain
|
||||||
|
uint256 txnHash_;
|
||||||
|
TER ter_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
* `RefundTransferResult`. This is added when a federator detects the end of a
|
||||||
|
refund transactions. Refunds may occur if there is an error transfering funds
|
||||||
|
at the end of a cross chain transaction.
|
||||||
|
```c++
|
||||||
|
struct RefundTransferResult
|
||||||
|
{
|
||||||
|
// direction is the direction of the triggering transaction.
|
||||||
|
// I.e. A "mainToSide" refund transfer result is a transaction that
|
||||||
|
// happens on the mainchain (the triggering transaction happended on the
|
||||||
|
// mainchain, the failed result happened on the side chain, and the refund
|
||||||
|
// result happened on the mainchain)
|
||||||
|
Dir dir_;
|
||||||
|
AccountID dst_;
|
||||||
|
std::optional<STAmount> deliveredAmt_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
// Txn hash of the initiating xchain transaction
|
||||||
|
uint256 srcChainTxnHash_;
|
||||||
|
// Txn hash of the federator's transaction on the dst chain
|
||||||
|
uint256 dstChainTxnHash_;
|
||||||
|
// Txn hash of the refund result
|
||||||
|
uint256 txnHash_;
|
||||||
|
TER ter_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
* `TicketCreateResult`. This is added when the federator detects a ticket create
|
||||||
|
transaction.
|
||||||
|
|
||||||
|
```
|
||||||
|
struct TicketCreateResult
|
||||||
|
{
|
||||||
|
Dir dir_;
|
||||||
|
bool success_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
std::uint32_t ledgerIndex_;
|
||||||
|
uint256 srcChainTxnHash_;
|
||||||
|
uint256 txnHash_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
std::uint32_t sourceTag_;
|
||||||
|
std::string memoStr_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
|
||||||
|
void
|
||||||
|
removeTrigger();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
* `DepositAuthResult`. This is added when the federator detects a deposit auth
|
||||||
|
transaction. Deposit auth is used to pause cross chain transactions if the
|
||||||
|
federators fall too far behind.
|
||||||
|
```
|
||||||
|
struct DepositAuthResult
|
||||||
|
{
|
||||||
|
Dir dir_;
|
||||||
|
bool success_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
std::uint32_t ledgerIndex_;
|
||||||
|
uint256 srcChainTxnHash_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
AccountFlagOp op_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
* `BootstrapTicket`. This is added when the federator detects one of the initial
|
||||||
|
ticket transactions that is added during account setup.
|
||||||
|
```
|
||||||
|
struct BootstrapTicket
|
||||||
|
{
|
||||||
|
bool isMainchain_;
|
||||||
|
bool success_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
std::uint32_t ledgerIndex_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
std::uint32_t sourceTag_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
* `DisableMasterKeyResult`. This is added when the federator detects an
|
||||||
|
`AccountSet` transaction that disables the master key. Transactions that come
|
||||||
|
before this are assumed to be part of account setup.
|
||||||
|
```
|
||||||
|
struct DisableMasterKeyResult
|
||||||
|
{
|
||||||
|
bool isMainchain_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
* `HeartbeatTimer`. This is added at regular intervals and is used to trigger
|
||||||
|
evants based on timeouts.
|
||||||
|
|
||||||
|
### Federator Event Handling
|
||||||
|
|
||||||
|
Handling the events is very simple. The `mainLoop` pops events off the event
|
||||||
|
queue and dispatches it to an event handler. There is one event handler for each
|
||||||
|
event type. There is also some logic to prevent busy waiting.
|
||||||
|
|
||||||
|
```c++
|
||||||
|
void
|
||||||
|
onEvent(event::XChainTransferDetected const& e);
|
||||||
|
void
|
||||||
|
onEvent(event::XChainTransferResult const& e);
|
||||||
|
void
|
||||||
|
onEvent(event::RefundTransferResult const& e);
|
||||||
|
void
|
||||||
|
onEvent(event::HeartbeatTimer const& e);
|
||||||
|
|
||||||
|
void
|
||||||
|
Federator::mainLoop()
|
||||||
|
{
|
||||||
|
FederatorEvent event;
|
||||||
|
while (!requestStop_)
|
||||||
|
{
|
||||||
|
if (!events_.pop(event))
|
||||||
|
{
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
// In rare cases, an event may be pushed and the condition
|
||||||
|
// variable signaled before the condition variable is waited on.
|
||||||
|
// To handle this, set a timeout on the wait.
|
||||||
|
std::unique_lock l{m_};
|
||||||
|
cv_.wait_for(
|
||||||
|
l, 1s, [this] { return requestStop_ || !events_.empty(); });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::visit([this](auto&& e) { this->onEvent(e); }, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Events are added to a queue in the `push` method.
|
||||||
|
```c++
|
||||||
|
void
|
||||||
|
Federator::push(FederatorEvent const& e)
|
||||||
|
{
|
||||||
|
bool const notify = events_.empty();
|
||||||
|
events_.push(e);
|
||||||
|
if (notify)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> l(m_);
|
||||||
|
cv_.notify_one();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Due the threading and lifetime issues, `Federator` is kept as a `shared_ptr`
|
||||||
|
inside of the app and enables `shared_from_this`. Since `shared_from_this`
|
||||||
|
cannot be used from a constructor, it uses two-phase initialization of a
|
||||||
|
constructor and an `init` function. These constructors are private, and objects
|
||||||
|
are created with a `make_federator` function that implements the two-phase
|
||||||
|
initialization. (Note: since `make_shared` cannot call a private constructor, a
|
||||||
|
private `PrivateTag` is used instead.)
|
||||||
|
|
||||||
|
### Federator Listeners
|
||||||
|
|
||||||
|
There are two listener classes: a `MainchainListener` and a `SidechainListener`
|
||||||
|
(both inherit from a common `ChainListener` class where much of the
|
||||||
|
implementation lives). These classes monitor the chains for transactions on the
|
||||||
|
door accounts and add the appropriate event to the federator. The
|
||||||
|
`MainchainListener` uses a websocket to monitor transactions. Since the
|
||||||
|
federator lives in the same executable as the side chain validators,
|
||||||
|
`SidechainListener` used `InfoSub` directly rather than a websocket.
|
||||||
|
|
||||||
|
The federator is kept as `weak_ptr` in this class. Since these class will be used
|
||||||
|
as part of a callback from different threads, both listener classes enable
|
||||||
|
`shared_from_this`.
|
||||||
|
|
||||||
|
### Federator WebsocketClient
|
||||||
|
|
||||||
|
The `WebsocketClient` class takes an asio `io_service`, main chain server ip and
|
||||||
|
port, and callback. When a command response or new stream result is received,
|
||||||
|
the callback is executed (is will be called from the `io_service` thread). The
|
||||||
|
`MainchainListener` uses this class to listen for transactions.
|
||||||
|
This class is also be used to send transactions to the main chain.
|
||||||
|
```c++
|
||||||
|
WebsocketClient(
|
||||||
|
std::function<void(Json::Value const&)> callback,
|
||||||
|
boost::asio::io_service& ios,
|
||||||
|
boost::asio::ip::address const& ip,
|
||||||
|
std::uint16_t port,
|
||||||
|
std::unordered_map<std::string, std::string> const& headers = {});
|
||||||
|
```
|
||||||
|
|
||||||
|
# Triggering Transaction Handler
|
||||||
|
|
||||||
|
The triggering transaction handler is part of a single-threaded event loop that
|
||||||
|
responds to events triggered from the different chains. It is single-threaded so
|
||||||
|
there's no danger of events being processed out-of-order and for simplicity of
|
||||||
|
code. Note that event handlers are not computationally intensive, so there would
|
||||||
|
be little benefit to multiple threads.
|
||||||
|
|
||||||
|
When a new event is handled, a response transaction may be prepared. Apart from
|
||||||
|
the sequence number, all the values of a response transaction can be determined
|
||||||
|
from the trigging transaction, and it does not require communicating with the
|
||||||
|
other federators. For example, when sending assets between chains, the response
|
||||||
|
transaction will contain an amount equal to the XRP `delivered_amt` in the
|
||||||
|
triggering transaction, a fixed fee, and a memo with the hash of the cross chain
|
||||||
|
transaction. The memo is important because it creates a transaction unique to
|
||||||
|
the triggering cross chain transaction, and it safer to sign such a transaction
|
||||||
|
in case sequence numbers somehow get out of sync between the federators. The
|
||||||
|
`LastLedgerSequence` field is not be set.
|
||||||
|
|
||||||
|
Next the federator will sign these transactions and send its signature to its
|
||||||
|
peers. This happens in another thread so the event loop isn't slowed down.
|
||||||
|
|
||||||
|
Next the federator adds its signatures to the txn in the `pending transactions`
|
||||||
|
collection for the appropriate chain. See [Adding a signature to Pending
|
||||||
|
Transactions](#adding-a-signature-to-pending-transactions)
|
||||||
|
|
||||||
|
If it doesn't have enough signatures to complete the multi-signature, the
|
||||||
|
federator will add signatures by listening for signature messages from other
|
||||||
|
peers see the [Collecting Signatures](#collecting-signatures) section.
|
||||||
|
|
||||||
|
## Collecting Signatures
|
||||||
|
|
||||||
|
A federator receives signatures by listening to peer messages. Signatures are
|
||||||
|
automatically sent when a federator detects a new cross chain transaction.
|
||||||
|
|
||||||
|
When a federator receives a new signature, it forwards it to its peers that have
|
||||||
|
not already received this signature from this federator.
|
||||||
|
|
||||||
|
Next it checks if this transaction has already been handled. If so, it does
|
||||||
|
nothing further.
|
||||||
|
|
||||||
|
Next the federator adds the signature to the txn in the `pending transactions`
|
||||||
|
collection. See [Adding a signature to Pending
|
||||||
|
Transactions](#adding-a-signature-to-pending-transactions).
|
||||||
|
|
||||||
|
Note that a federator may receive multiple signatures for the same transaction
|
||||||
|
but with different sequence numbers. This should only happen if a federator
|
||||||
|
somehow has the wrong sequence number and is later corrected.
|
||||||
|
|
||||||
|
## Adding a signature to Pending Transactions
|
||||||
|
|
||||||
|
The `pending transactions` collections stores the transactions that do not yet
|
||||||
|
have enough signatures to be submitted or have not been confirmed as sent. There
|
||||||
|
is one collection for the main chain and one for the side chain. The key to this
|
||||||
|
collection is a hash of the triggering transaction, but with a sequence of zero
|
||||||
|
and a fee of zero (remember that this transaction has a memo field with the hash
|
||||||
|
of the cross chain transaction, so it is unique). It is hashed this way to
|
||||||
|
detect inconsistent sequence numbers and fees. The `value` of this collection is
|
||||||
|
a struct that contains this federators signature (if available) and another map
|
||||||
|
with a `key` of sequence number and fee, and a `value` of a collection of
|
||||||
|
signatures. Before adding a signature to this collection, if the signature is
|
||||||
|
not on the multi-signature list, it is discarded. The signature is also checked
|
||||||
|
for validity. If it is invalid, it is discarded.
|
||||||
|
|
||||||
|
After adding a signature to this collection it checks if it this signature is
|
||||||
|
for a transaction with the same sequence number and fee as this federator, and
|
||||||
|
if has enough signatures for a valid multi-signed transaction. If so, the
|
||||||
|
transactions are added to the `queued transactions` collection for the
|
||||||
|
appropriate chain and the function to submit transactions is called (see
|
||||||
|
[Transaction Submit](#transaction-submit)).
|
||||||
|
|
||||||
|
If the transaction has enough signature for a valid multi-signed transaction,
|
||||||
|
and the sequence and fee _do not_ match the ones from this federator, then this
|
||||||
|
federator sequence number must be out of sync with the rest of the network. If
|
||||||
|
it is detected, the federator will correct its sequence number. Note that there
|
||||||
|
may be other transactions that have been submitted since this transaction, so
|
||||||
|
the sequence number needs to be appropriately adjusted to account for this.
|
||||||
|
Inconsistent fee will not be handled in the prototype. The fee will always be a
|
||||||
|
constant. This federator will also change its signature for this transaction and
|
||||||
|
submit it to the network (see [Transaction Submit](#transaction-submit)). This
|
||||||
|
new signature will be broadcast to the network.
|
||||||
|
|
||||||
|
A heartbeat timer event will periodically check the collection for transactions
|
||||||
|
in the queue older than some threshold. When these are detected, a error will be
|
||||||
|
printed to the log. If the federator knows the transaction has already been
|
||||||
|
handled by the network, it will be removed from the queue.
|
||||||
|
|
||||||
|
## Transaction Submit
|
||||||
|
|
||||||
|
There is a limit on the number of transaction in flight at any time. While the
|
||||||
|
number of transactions is below this limit, and the next transaction in the
|
||||||
|
sequence is part of the `queued transactions` collection, send the response
|
||||||
|
transaction to the appropriate chain. Once a transaction is sent, it is removed
|
||||||
|
from the `queued transactions` collection. However, it remains part of the
|
||||||
|
`pending transactions` collection until a response transaction result is
|
||||||
|
observed.
|
||||||
|
|
||||||
|
Note that limiting the number of transactions in flight to one makes for a
|
||||||
|
simpler design, but greatly limits the throughput of cross-chain transactions.
|
||||||
|
|
||||||
|
## Handling Response Transaction Results
|
||||||
|
|
||||||
|
Response transaction results are used to know when a response transaction has
|
||||||
|
been handled and can be removed from the `pending transactions` collection. It
|
||||||
|
is also used to issue refunds when a response transaction fails under some
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
If a transaction is fails is anything other than `tefAlready`, then a new
|
||||||
|
response transaction is created that refunds some protion of the origional
|
||||||
|
amount to the origional sending account. Gathering signatures for this refund
|
||||||
|
transaction is the same as what's done other triggering transactions. Much like
|
||||||
|
sending an asset cross-chain, transactions that trigger refunds and their
|
||||||
|
response transations happen on different chains. This means we can use the same
|
||||||
|
algorithm to assign sequence numbers to the response transaction.
|
||||||
|
|
||||||
|
If a transaction fails with a `tefAlready`, that means another federator already
|
||||||
|
submitted the transaction. Ignore this error.
|
||||||
|
|
||||||
|
There is also a timer that checks for transactions that have not had results for
|
||||||
|
too long of a time. If they are detected, an error is logged, but the prototype
|
||||||
|
does not attempt to handle the error further.
|
||||||
|
|
||||||
|
## Assigning sequence numbers
|
||||||
|
|
||||||
|
A federator keeps a variable that gives the next sequence number to assign to a
|
||||||
|
response transaction. When a new triggering transaction is detected, this
|
||||||
|
sequence number is given to the response transaction and incremented. How the
|
||||||
|
initial value is assigned is described in the [Getting a federator in
|
||||||
|
sync](#getting-a-federator-in-sync) section. How an incorrect sequence number is
|
||||||
|
corrected is described in the [Adding a signature to Pending
|
||||||
|
Transactions](#adding-a-signature-to-pending-transactions) section.
|
||||||
|
|
||||||
|
## Assigning fees
|
||||||
|
|
||||||
|
Side chain fees are burned, so this balance can never be redeemed through normal
|
||||||
|
cross chain transactions. If we wanted, these burned fees could be made
|
||||||
|
available to the federators by withdrawing XRP from the main chain account.
|
||||||
|
|
||||||
|
Given these fees are burned and are (in effect) payed by the account doing a
|
||||||
|
cross chain transaction, I propose these fees should be set on startup and kept
|
||||||
|
constant. Initial implementations will use a 20 drop fee.
|
||||||
|
|
||||||
|
## Specifying a cross-chain destination address
|
||||||
|
|
||||||
|
When sending a cross chain payment, the destination on the originating chain is
|
||||||
|
the special "door" account controlled by the federators. How does the user
|
||||||
|
specify what the destination address on the side chain should be? There are two
|
||||||
|
ways:
|
||||||
|
|
||||||
|
1) Specify the destination as a memo.
|
||||||
|
2) Assign tags to destinations.
|
||||||
|
|
||||||
|
The memo field can always be used to specify a destination. In addition, when a
|
||||||
|
new account is created, a new mapping is assigned between a tag and an address.
|
||||||
|
With a main to side transaction, the new tag will map to the newly created side
|
||||||
|
chain account.
|
||||||
|
|
||||||
|
The "tags" scheme is not yet designed. For now, the implementation will always
|
||||||
|
use the memo field to specify the destination address.
|
||||||
|
|
||||||
|
If an account sends an asset to the door account without specifying an address, a
|
||||||
|
refund will be issued to the sending account.
|
||||||
|
|
||||||
|
## Setting up the door accounts
|
||||||
|
|
||||||
|
The root account on the side chain is used as the door account. The door account
|
||||||
|
on the main chain is just a regular account. The following transactions must be
|
||||||
|
sent to these accounts before a running the federators:
|
||||||
|
|
||||||
|
* `SignerListSet`: Since the federators will jointly control these accounts, a
|
||||||
|
`SignerListSet` transaction must be sent to both the main chain account and
|
||||||
|
the side chain account. The signer list should consist of the federator's
|
||||||
|
public signing keys and should match the keys specified in the config file.
|
||||||
|
The quorum should be set to 80% of the federators on the list (i.e. for five
|
||||||
|
federators, set this to 4).
|
||||||
|
The federators use tickets to handle unusual situations. For example, if the
|
||||||
|
federators fall too far behind they will disallow new cross chain transactions
|
||||||
|
until they catch up. Three tickets are needed, and three transactions are needed
|
||||||
|
to create the tickets (since they use the source tag as a way to set the purpose
|
||||||
|
for the ticket).
|
||||||
|
|
||||||
|
* `Ticket`: Sent a `Ticket` transaction with the source tag of `1` to both the
|
||||||
|
main chain account and side chain account.
|
||||||
|
|
||||||
|
* `Ticket`: Sent a `Ticket` transaction with the source tag of `2` to both the
|
||||||
|
main chain account and side chain account.
|
||||||
|
|
||||||
|
* `Ticket`: Sent a `Ticket` transaction with the source tag of `3` to both the
|
||||||
|
main chain account and side chain account.
|
||||||
|
|
||||||
|
* `TrustSet` if the cross chain transactions involve issued assets (IOUs), set
|
||||||
|
up the trust lines by sending a `TrustSet` transaction to the appropriate
|
||||||
|
accounts. If the cross chain transactions only involve XRP, this is not
|
||||||
|
needed.
|
||||||
|
|
||||||
|
* `AccountSet`: Disable the master key with an `AccountSet` transactions. This
|
||||||
|
ensures that nothing except the federators (as a group) control these
|
||||||
|
accounts. Send this transaction to both the main chain account and side chain
|
||||||
|
account.
|
||||||
|
|
||||||
|
*Important*: The `AccountSet` transaction that disables the master key *must* be
|
||||||
|
the last transaction. The federator's initialization code uses this to
|
||||||
|
distinguish transactions that are part of setup and other transactions.
|
||||||
|
|
||||||
|
## Handling a crashed main chain server
|
||||||
|
|
||||||
|
The side chain depends on a websocket to main chain server to listen for
|
||||||
|
transactions. If the main chain server disconnects from the side chain a
|
||||||
|
fail-over option should be added. This would allow a federator to automatically
|
||||||
|
connect to other main chain servers if the one it is currency connected to goes
|
||||||
|
down.
|
||||||
|
|
||||||
|
## Federator throughput
|
||||||
|
|
||||||
|
On average, federators must process cross chain transactions faster than they
|
||||||
|
occur. If there are 10 cross chain transactions per ledger, but the federators
|
||||||
|
can only sign and submit 5 response transactions per ledger, the federators will
|
||||||
|
keep falling father behind and will never catch up. Monitoring the size of the
|
||||||
|
queues can detect this situation, but there is no good remedy for it. The rate
|
||||||
|
of cross chain transactions are out of it's control.
|
||||||
|
|
||||||
|
If this situation occurs, new transactions to the door account will be disabled
|
||||||
|
with a "deposit auth" transaction, and transactions will be disabled until the
|
||||||
|
federators catch up. Because this transaction is not in response to a triggering
|
||||||
|
transaction on the "opposite" chain, assigning a sequence number for the
|
||||||
|
"deposit auth" transaction is more involved. We use the protocol
|
||||||
|
in [Handling an unordered event](#Handling-an-unordered-event) section to submit
|
||||||
|
the transaction.
|
||||||
|
|
||||||
|
## Handling an unordered event
|
||||||
|
|
||||||
|
Cross chain payment transactions from one chain are sorted by that chain's
|
||||||
|
consensus protocol. Each transaction results in one payment transaction in the
|
||||||
|
destination chain, hence consuming one sequence number of the door account in
|
||||||
|
the destination chain. Staring from the same initial sequence number assigned
|
||||||
|
when the account was created, and processing the same stream of payment
|
||||||
|
transactions, the federators agree on which sequence number to use for a given
|
||||||
|
payment transaction without communication.
|
||||||
|
|
||||||
|
From time to time, however, federators have to create transactions to process
|
||||||
|
unordered events, such as temporarily "disable" a door account with a "deposit
|
||||||
|
auth" AccountSet transaction, or update the door accounts signerLists with
|
||||||
|
SignerListSet transactions. Assigning sequence number to these transactions are
|
||||||
|
more involved than payment transactions, because these events are not sorted
|
||||||
|
with themselves nor with payment transactions. Since they are not sorted, there
|
||||||
|
is a chance that different federators use different sequence numbers. If
|
||||||
|
different sequence numbers were used for a transaction, this transaction and
|
||||||
|
(depending on the design) some transactions following it won't be processed. So
|
||||||
|
for these transactions, tickets are used to assign sequence numbers. Tickets are
|
||||||
|
reserved when the side chain first starts (on genesis), and tickets are
|
||||||
|
replenished as they are used.
|
||||||
|
|
||||||
|
Our first ticket based protocol used tickets for both the transaction that
|
||||||
|
handles an unordered event and the "TicketCreate" transaction to create new
|
||||||
|
tickets. It was simple but had two issues: the sequence numbers allocated to
|
||||||
|
renewed tickets and the sequence numbers used for payment transactions occurred
|
||||||
|
at the same time may overlap so the payment transactions must be modified,
|
||||||
|
resigned and resubmitted; there is also a small chance that the payment
|
||||||
|
transactions are delivered to the destination chain out of order. Our current
|
||||||
|
design grows from the first design. We use a pre-allocated ticket pair, one main
|
||||||
|
chain ticket and one side chain ticket, to submit "no-op" transactions to both
|
||||||
|
chains. Once they are processed by the chains' consensus and sorted with payment
|
||||||
|
transactions, in later rounds of the protocol, both the "TicketCreate"
|
||||||
|
transactions to create new tickets and the transaction(s) that handles an
|
||||||
|
unordered event will use real sequence numbers instead of tickets. Hence we
|
||||||
|
avoid both the issues of the first design. The current design is shown in the
|
||||||
|
diagram below.
|
||||||
|
|
||||||
|
The top portion of the diagram shows (simplified) payment process sequence. Note
|
||||||
|
that the only usage of the side chain sequence numbers is for processing
|
||||||
|
payments already sorted by main chain consensus, and vice versa. The bottom
|
||||||
|
portion of the diagram shows the 3-round protocol for processing unordered
|
||||||
|
events (round 2 and 3 could be merged). Note that the side chain sequence
|
||||||
|
numbers still have a single usage, i.e. processing transactions already sorted
|
||||||
|
by main chain consensus, and vice versa.
|
||||||
|
|
||||||
|
In more detail, the first round of the protocol uses a ticket pair to send no-op
|
||||||
|
transactions to both of the chains. In the second round, the inclusion of the
|
||||||
|
no-op transactions in the ledgers means they are sorted together with other
|
||||||
|
transactions. Since they are sorted, the sequence numbers of their corresponding
|
||||||
|
next round transactions (TicketCreate) are agreed by the federators. The
|
||||||
|
federators can also predict the ticket numbers that will be allocated once the
|
||||||
|
ticketCreate transactions are processed by the consensus. Hence they will not
|
||||||
|
use those sequence numbers for other purposes. In the final round of the
|
||||||
|
protocol, the new tickets are indeed created and the ticket pair are refilled,
|
||||||
|
and the transaction(s) that handles the unordered event can take a sequence
|
||||||
|
number and submit to its destination chain.
|
||||||
|
|
||||||
|
This protocol can be used for one-sided events such as "disable" main chain
|
||||||
|
account temporarily, or two-sided events such as update the signerLists of both
|
||||||
|
door accounts. To avoid a race resulted from multiple events compete the same
|
||||||
|
ticket pair, every ticket pair has a "purpose" so that the pair can only be used
|
||||||
|
for one type of events. Currently three purposes are implemented, or planned.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Federator as a separate program
|
||||||
|
|
||||||
|
For the prototype, the federators will be validators on the side chain. However,
|
||||||
|
a federator can be independent from both chains. The federator can listen for
|
||||||
|
side chain transactions with a websocket, just like it does for the main chain.
|
||||||
|
The `last submitted txn` and `last confirmed txn` values can be kept by the
|
||||||
|
federators themselves.
|
||||||
|
|
||||||
|
The biggest advantage to combining a federator and a validator is to re-use the
|
||||||
|
overlay layer. This saves on implementation time in the prototype. Longer term,
|
||||||
|
it makes sense to separate a federator and a validator.
|
||||||
|
|
||||||
|
# Config file changes
|
||||||
|
|
||||||
|
See [this](configFile.md) document for the config file stanzas used to support
|
||||||
|
side chains.
|
||||||
|
|
||||||
|
# New ledger objects
|
||||||
|
|
||||||
|
Notice that side chains do not require any new ledger objects, and do not
|
||||||
|
require federators to communicate in order to agree on transaction values.
|
||||||
|
|
||||||
|
# New RPC commands
|
||||||
|
|
||||||
|
* "account_history_tx_stream" is used to get historic transactions
|
||||||
|
when syncing and get new transactions once synced.
|
||||||
|
|
||||||
|
* "Federator info" is used to get information about the state of a federator,
|
||||||
|
including its sync state and the state of its transaction queues.
|
||||||
50
docs/sidechain/federatorAccountSetup.md
Normal file
50
docs/sidechain/federatorAccountSetup.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
## Introduction
|
||||||
|
|
||||||
|
Side chain federators work by controlling an account on the main chain and an
|
||||||
|
account on the side chain. The account on the side chain is the root account.
|
||||||
|
The account on the main chain is specified in the configuration file (See
|
||||||
|
[configFile.md](docs/sidechain/configFile.md) for the new configuration file
|
||||||
|
stanzas).
|
||||||
|
|
||||||
|
The test scripts will set up these accounts for you when running a test network
|
||||||
|
on your local machine (see the functions `setup_mainchain` and `setup_sidechain`
|
||||||
|
in the sidechain.py module). This document describes what's needed to set up
|
||||||
|
these accounts if not using the scripts.
|
||||||
|
|
||||||
|
## Transactions
|
||||||
|
|
||||||
|
* `SignerListSet`: Since the federators will jointly control these accounts, a
|
||||||
|
`SignerListSet` transaction must be sent to both the main chain account and
|
||||||
|
the side chain account. The signer list should consist of the federator's
|
||||||
|
public signing keys and should match the keys specified in the config file.
|
||||||
|
The quorum should be set to 80% of the federators on the list (i.e. for five
|
||||||
|
federators, set this to 4).
|
||||||
|
|
||||||
|
The federators use tickets to handle unusual situations. For example, if the
|
||||||
|
federators fall too far behind they will disallow new cross chain transactions
|
||||||
|
until they catch up. Three tickets are needed, and three transactions are needed
|
||||||
|
to create the tickets (since they use the source tag as a way to set the purpose
|
||||||
|
for the ticket).
|
||||||
|
|
||||||
|
* `Ticket`: Sent a `Ticket` transaction with the source tag of `1` to both the
|
||||||
|
main chain account and side chain account.
|
||||||
|
|
||||||
|
* `Ticket`: Sent a `Ticket` transaction with the source tag of `2` to both the
|
||||||
|
main chain account and side chain account.
|
||||||
|
|
||||||
|
* `Ticket`: Sent a `Ticket` transaction with the source tag of `3` to both the
|
||||||
|
main chain account and side chain account.
|
||||||
|
|
||||||
|
* `TrustSet` if the cross chain transactions involve issued assets (IOUs), set
|
||||||
|
up the trust lines by sending a `TrustSet` transaction to the appropriate
|
||||||
|
accounts. If the cross chain transactions only involve XRP, this is not
|
||||||
|
needed.
|
||||||
|
|
||||||
|
* `AccountSet`: Disable the master key with an `AccountSet` transactions. This
|
||||||
|
ensures that nothing except the federators (as a group) control these
|
||||||
|
accounts. Send this transaction to both the main chain account and side chain
|
||||||
|
account.
|
||||||
|
|
||||||
|
*Important*: The `AccountSet` transaction that disables the master key *must* be
|
||||||
|
the last transaction. The federator's initialization code uses this to
|
||||||
|
distinguish transactions that are part of setup and other transactions.
|
||||||
BIN
docs/sidechain/ticketsAndSeq.png
Normal file
BIN
docs/sidechain/ticketsAndSeq.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 182 KiB |
76
docs/sidechain/ticketsAndSeq.puml
Normal file
76
docs/sidechain/ticketsAndSeq.puml
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
@startuml
|
||||||
|
|
||||||
|
participant "main chain network" as mc #LightGreen
|
||||||
|
box "Federator"
|
||||||
|
participant "**main** chain\ndoor account listener" as mdl
|
||||||
|
participant "**main** chain\nsignature collector" as msc
|
||||||
|
participant "**main** chain\nsequence number" as msn #LightBlue
|
||||||
|
participant "ticket pair" as t #LightBlue
|
||||||
|
participant "unordered event" as ue #LightCoral
|
||||||
|
participant "**side** chain\nsequence number" as ssn #LightBlue
|
||||||
|
participant "**side** chain\nsignature collector" as ssc
|
||||||
|
participant "**side** chain\ndoor account listener" as sdl
|
||||||
|
end box
|
||||||
|
participant "side chain network" as sc #LightGreen
|
||||||
|
actor "federator admin" as fa #LightGreen
|
||||||
|
|
||||||
|
== payments ==
|
||||||
|
group cross chain payment to side chain
|
||||||
|
mc -> mdl: payment tx to door account\nin ledger
|
||||||
|
mdl -> ssn: side chain door account payment tx created
|
||||||
|
ssn -> ssc: sequence number filled
|
||||||
|
ssc -> ssc: payment tx signed,\ncollect signatures
|
||||||
|
ssc -> sc : with quorum signatures\nsubmit tx to network
|
||||||
|
end
|
||||||
|
group cross chain payment to main chain
|
||||||
|
sc -> sdl: payment tx to door account\nin ledger
|
||||||
|
sdl -> msn: main chain door account payment tx created
|
||||||
|
msn -> msc: sequence number filled
|
||||||
|
msc -> msc: payment tx signed,\ncollect signatures
|
||||||
|
msc -> mc : with quorum signatures\nsubmit tx to network
|
||||||
|
end
|
||||||
|
|
||||||
|
== unordered events ==
|
||||||
|
group round 1
|
||||||
|
fa -> ue : misc request from admin\nor federator internal event\nE.g. close main chain door account due to high load
|
||||||
|
ue -> t : **two** no-op AccountSet txns created\n(for trigger ticketCreate txns round 2)
|
||||||
|
activate t
|
||||||
|
t -> msc: ticket number filled
|
||||||
|
t -> ssc: ticket number filled
|
||||||
|
deactivate t
|
||||||
|
msc -> msc: no-op AccountSet tx signed,\ncollect signatures
|
||||||
|
ssc -> ssc: no-op AccountSet tx signed,\ncollect signatures
|
||||||
|
msc -> mc : with quorum signatures\nsubmit tx to network
|
||||||
|
ssc -> sc : with quorum signatures\nsubmit tx to network
|
||||||
|
end
|
||||||
|
|
||||||
|
group round 2
|
||||||
|
'== unordered event, round 2 ==
|
||||||
|
mc -> mdl: no-op AccountSet in ledger
|
||||||
|
sc -> sdl: no-op AccountSet in ledger
|
||||||
|
mdl -> ssn: create side chain door account ticketCreate tx\nto allocate side chain door account ticket\nto refill ticket pair
|
||||||
|
sdl -> msn: create main chain door account ticketCreate tx\nto allocate main chain door account ticket\nto refill ticket pair
|
||||||
|
ssn -> ssc: sequence number filled
|
||||||
|
msn -> msc: sequence number filled
|
||||||
|
ssc -> ssc: ticketCreate tx signed,\ncollect signatures
|
||||||
|
msc -> msc: ticketCreate tx signed,\ncollect signatures
|
||||||
|
ssc -> sc : with quorum signatures\nsubmit tx to network
|
||||||
|
msc -> mc : with quorum signatures\nsubmit tx to network
|
||||||
|
end
|
||||||
|
|
||||||
|
group round 3
|
||||||
|
'== unordered event, round 3 ==
|
||||||
|
mc -> mdl: ticketCreate in ledger
|
||||||
|
mdl -> t : refill
|
||||||
|
sc -> sdl: ticketCreate in ledger
|
||||||
|
activate sdl
|
||||||
|
sdl -> t : refill
|
||||||
|
sdl -> msn: main chain deposit-auth AccountSet created
|
||||||
|
note left: assuming the unordered event is to\nclose main chain door account\nto block new payments temporarily
|
||||||
|
deactivate sdl
|
||||||
|
msn -> msc: sequence number filled
|
||||||
|
msc -> msc: deposit-auth AccountSet tx signed,\ncollect signatures
|
||||||
|
msc -> mc : with quorum signatures\nsubmit tx to network
|
||||||
|
end
|
||||||
|
|
||||||
|
@enduml
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
#include <ripple/app/rdb/RelationalDBInterface_global.h>
|
#include <ripple/app/rdb/RelationalDBInterface_global.h>
|
||||||
#include <ripple/app/rdb/backend/RelationalDBInterfacePostgres.h>
|
#include <ripple/app/rdb/backend/RelationalDBInterfacePostgres.h>
|
||||||
#include <ripple/app/reporting/ReportingETL.h>
|
#include <ripple/app/reporting/ReportingETL.h>
|
||||||
|
#include <ripple/app/sidechain/Federator.h>
|
||||||
#include <ripple/app/tx/apply.h>
|
#include <ripple/app/tx/apply.h>
|
||||||
#include <ripple/basics/ByteUtilities.h>
|
#include <ripple/basics/ByteUtilities.h>
|
||||||
#include <ripple/basics/PerfLog.h>
|
#include <ripple/basics/PerfLog.h>
|
||||||
@@ -215,6 +216,8 @@ public:
|
|||||||
RCLValidations mValidations;
|
RCLValidations mValidations;
|
||||||
std::unique_ptr<LoadManager> m_loadManager;
|
std::unique_ptr<LoadManager> m_loadManager;
|
||||||
std::unique_ptr<TxQ> txQ_;
|
std::unique_ptr<TxQ> txQ_;
|
||||||
|
|
||||||
|
std::shared_ptr<sidechain::Federator> sidechainFederator_;
|
||||||
ClosureCounter<void, boost::system::error_code const&> waitHandlerCounter_;
|
ClosureCounter<void, boost::system::error_code const&> waitHandlerCounter_;
|
||||||
boost::asio::steady_timer sweepTimer_;
|
boost::asio::steady_timer sweepTimer_;
|
||||||
boost::asio::steady_timer entropyTimer_;
|
boost::asio::steady_timer entropyTimer_;
|
||||||
@@ -519,6 +522,8 @@ public:
|
|||||||
checkSigs(bool) override;
|
checkSigs(bool) override;
|
||||||
bool
|
bool
|
||||||
isStopping() const override;
|
isStopping() const override;
|
||||||
|
void
|
||||||
|
startFederator() override;
|
||||||
int
|
int
|
||||||
fdRequired() const override;
|
fdRequired() const override;
|
||||||
|
|
||||||
@@ -889,6 +894,12 @@ public:
|
|||||||
return *mWalletDB;
|
return *mWalletDB;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<sidechain::Federator>
|
||||||
|
getSidechainFederator() override
|
||||||
|
{
|
||||||
|
return sidechainFederator_;
|
||||||
|
}
|
||||||
|
|
||||||
ReportingETL&
|
ReportingETL&
|
||||||
getReportingETL() override
|
getReportingETL() override
|
||||||
{
|
{
|
||||||
@@ -1057,6 +1068,8 @@ public:
|
|||||||
ledgerCleaner_->stop();
|
ledgerCleaner_->stop();
|
||||||
if (reportingETL_)
|
if (reportingETL_)
|
||||||
reportingETL_->stop();
|
reportingETL_->stop();
|
||||||
|
if (sidechainFederator_)
|
||||||
|
sidechainFederator_->stop();
|
||||||
if (auto pg = dynamic_cast<RelationalDBInterfacePostgres*>(
|
if (auto pg = dynamic_cast<RelationalDBInterfacePostgres*>(
|
||||||
&*mRelationalDBInterface))
|
&*mRelationalDBInterface))
|
||||||
pg->stop();
|
pg->stop();
|
||||||
@@ -1163,6 +1176,8 @@ public:
|
|||||||
getLedgerReplayer().sweep();
|
getLedgerReplayer().sweep();
|
||||||
m_acceptedLedgerCache.sweep();
|
m_acceptedLedgerCache.sweep();
|
||||||
cachedSLEs_.sweep();
|
cachedSLEs_.sweep();
|
||||||
|
if (sidechainFederator_)
|
||||||
|
sidechainFederator_->sweep();
|
||||||
|
|
||||||
#ifdef RIPPLED_REPORTING
|
#ifdef RIPPLED_REPORTING
|
||||||
if (auto pg = dynamic_cast<RelationalDBInterfacePostgres*>(
|
if (auto pg = dynamic_cast<RelationalDBInterfacePostgres*>(
|
||||||
@@ -1597,6 +1612,15 @@ ApplicationImp::setup()
|
|||||||
if (reportingETL_)
|
if (reportingETL_)
|
||||||
reportingETL_->start();
|
reportingETL_->start();
|
||||||
|
|
||||||
|
sidechainFederator_ = sidechain::make_Federator(
|
||||||
|
*this,
|
||||||
|
get_io_service(),
|
||||||
|
*config_,
|
||||||
|
logs_->journal("SidechainFederator"));
|
||||||
|
|
||||||
|
if (sidechainFederator_)
|
||||||
|
sidechainFederator_->start();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1621,6 +1645,7 @@ ApplicationImp::start(bool withTimers)
|
|||||||
grpcServer_->start();
|
grpcServer_->start();
|
||||||
ledgerCleaner_->start();
|
ledgerCleaner_->start();
|
||||||
perfLog_->start();
|
perfLog_->start();
|
||||||
|
startFederator();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -1678,6 +1703,14 @@ ApplicationImp::isStopping() const
|
|||||||
return isTimeToStop;
|
return isTimeToStop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ApplicationImp::startFederator()
|
||||||
|
{
|
||||||
|
if (sidechainFederator_)
|
||||||
|
sidechainFederator_->unlockMainLoop(
|
||||||
|
sidechain::Federator::UnlockMainLoopKey::app);
|
||||||
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
ApplicationImp::fdRequired() const
|
ApplicationImp::fdRequired() const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ namespace RPC {
|
|||||||
class ShardArchiveHandler;
|
class ShardArchiveHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace sidechain {
|
||||||
|
class Federator;
|
||||||
|
}
|
||||||
|
|
||||||
// VFALCO TODO Fix forward declares required for header dependency loops
|
// VFALCO TODO Fix forward declares required for header dependency loops
|
||||||
class AmendmentTable;
|
class AmendmentTable;
|
||||||
|
|
||||||
@@ -150,6 +154,9 @@ public:
|
|||||||
virtual bool
|
virtual bool
|
||||||
isStopping() const = 0;
|
isStopping() const = 0;
|
||||||
|
|
||||||
|
virtual void
|
||||||
|
startFederator() = 0;
|
||||||
|
|
||||||
//
|
//
|
||||||
// ---
|
// ---
|
||||||
//
|
//
|
||||||
@@ -274,6 +281,9 @@ public:
|
|||||||
virtual DatabaseCon&
|
virtual DatabaseCon&
|
||||||
getWalletDB() = 0;
|
getWalletDB() = 0;
|
||||||
|
|
||||||
|
virtual std::shared_ptr<sidechain::Federator>
|
||||||
|
getSidechainFederator() = 0;
|
||||||
|
|
||||||
/** Ensure that a newly-started validator does not sign proposals older
|
/** Ensure that a newly-started validator does not sign proposals older
|
||||||
* than the last ledger it persisted. */
|
* than the last ledger it persisted. */
|
||||||
virtual LedgerIndex
|
virtual LedgerIndex
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include <ripple/app/main/Application.h>
|
#include <ripple/app/main/Application.h>
|
||||||
#include <ripple/app/main/DBInit.h>
|
#include <ripple/app/main/DBInit.h>
|
||||||
#include <ripple/app/rdb/RelationalDBInterface_global.h>
|
#include <ripple/app/rdb/RelationalDBInterface_global.h>
|
||||||
|
#include <ripple/app/sidechain/Federator.h>
|
||||||
#include <ripple/basics/Log.h>
|
#include <ripple/basics/Log.h>
|
||||||
#include <ripple/basics/StringUtilities.h>
|
#include <ripple/basics/StringUtilities.h>
|
||||||
#include <ripple/basics/contract.h>
|
#include <ripple/basics/contract.h>
|
||||||
|
|||||||
2230
src/ripple/app/sidechain/Federator.cpp
Normal file
2230
src/ripple/app/sidechain/Federator.cpp
Normal file
File diff suppressed because it is too large
Load Diff
430
src/ripple/app/sidechain/Federator.h
Normal file
430
src/ripple/app/sidechain/Federator.h
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#ifndef RIPPLE_SIDECHAIN_FEDERATOR_H_INCLUDED
|
||||||
|
#define RIPPLE_SIDECHAIN_FEDERATOR_H_INCLUDED
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/FederatorEvents.h>
|
||||||
|
#include <ripple/app/sidechain/impl/DoorKeeper.h>
|
||||||
|
#include <ripple/app/sidechain/impl/MainchainListener.h>
|
||||||
|
#include <ripple/app/sidechain/impl/SidechainListener.h>
|
||||||
|
#include <ripple/app/sidechain/impl/SignatureCollector.h>
|
||||||
|
#include <ripple/app/sidechain/impl/SignerList.h>
|
||||||
|
#include <ripple/app/sidechain/impl/TicketHolder.h>
|
||||||
|
#include <ripple/basics/Buffer.h>
|
||||||
|
#include <ripple/basics/ThreadSaftyAnalysis.h>
|
||||||
|
#include <ripple/basics/UnorderedContainers.h>
|
||||||
|
#include <ripple/basics/base_uint.h>
|
||||||
|
#include <ripple/beast/utility/Journal.h>
|
||||||
|
#include <ripple/core/Config.h>
|
||||||
|
#include <ripple/json/json_value.h>
|
||||||
|
#include <ripple/protocol/AccountID.h>
|
||||||
|
#include <ripple/protocol/Quality.h>
|
||||||
|
#include <ripple/protocol/STAmount.h>
|
||||||
|
#include <ripple/protocol/SecretKey.h>
|
||||||
|
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
#include <boost/asio/deadline_timer.hpp>
|
||||||
|
#include <boost/container/flat_map.hpp>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <thread>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
|
||||||
|
class Application;
|
||||||
|
class STTx;
|
||||||
|
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
class Federator : public std::enable_shared_from_this<Federator>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum ChainType { sideChain, mainChain };
|
||||||
|
constexpr static size_t numChains = 2;
|
||||||
|
|
||||||
|
enum class UnlockMainLoopKey { app, mainChain, sideChain };
|
||||||
|
constexpr static size_t numUnlockMainLoopKeys = 3;
|
||||||
|
|
||||||
|
// These enums are encoded in the transaction. Changing the order will break
|
||||||
|
// backward compatibility. If a new type is added change txnTypeLast.
|
||||||
|
enum class TxnType { xChain, refund };
|
||||||
|
constexpr static std::uint8_t txnTypeLast = 2;
|
||||||
|
|
||||||
|
static constexpr std::uint32_t accountControlTxFee{1000};
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Tag so make_Federator can call `std::make_shared`
|
||||||
|
class PrivateTag
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
friend std::shared_ptr<Federator>
|
||||||
|
make_Federator(
|
||||||
|
Application& app,
|
||||||
|
boost::asio::io_service& ios,
|
||||||
|
BasicConfig const& config,
|
||||||
|
beast::Journal j);
|
||||||
|
|
||||||
|
std::thread thread_;
|
||||||
|
bool running_ = false;
|
||||||
|
std::atomic<bool> requestStop_ = false;
|
||||||
|
|
||||||
|
Application& app_;
|
||||||
|
std::array<AccountID, numChains> const account_;
|
||||||
|
std::array<std::atomic<std::uint32_t>, numChains> accountSeq_{1, 1};
|
||||||
|
std::array<std::atomic<std::uint32_t>, numChains> lastTxnSeqSent_{0, 0};
|
||||||
|
std::array<std::atomic<std::uint32_t>, numChains> lastTxnSeqConfirmed_{
|
||||||
|
0,
|
||||||
|
0};
|
||||||
|
std::array<std::atomic<bool>, numUnlockMainLoopKeys> unlockMainLoopKeys_{
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false};
|
||||||
|
std::shared_ptr<MainchainListener> mainchainListener_;
|
||||||
|
std::shared_ptr<SidechainListener> sidechainListener_;
|
||||||
|
|
||||||
|
mutable std::mutex eventsMutex_;
|
||||||
|
std::vector<FederatorEvent> GUARDED_BY(eventsMutex_) events_;
|
||||||
|
|
||||||
|
// When a user account sends an asset to the account controlled by the
|
||||||
|
// federator, the asset to be issued on the other chain is determined by the
|
||||||
|
// `assetProps` maps - one for each chain. The asset to be issued is
|
||||||
|
// `issue`, the amount of the asset to issue is determined by `quality`
|
||||||
|
// (ratio of output amount/input amount). When issuing refunds, the
|
||||||
|
// `refundPenalty` is subtracted from the sent amount before sending the
|
||||||
|
// refund.
|
||||||
|
struct OtherChainAssetProperties
|
||||||
|
{
|
||||||
|
Quality quality;
|
||||||
|
Issue issue;
|
||||||
|
STAmount refundPenalty;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::array<
|
||||||
|
boost::container::flat_map<Issue, OtherChainAssetProperties>,
|
||||||
|
numChains> const assetProps_;
|
||||||
|
|
||||||
|
PublicKey signingPK_;
|
||||||
|
SecretKey signingSK_;
|
||||||
|
|
||||||
|
// federator signing public keys
|
||||||
|
mutable std::mutex federatorPKsMutex_;
|
||||||
|
hash_set<PublicKey> GUARDED_BY(federatorPKsMutex_) federatorPKs_;
|
||||||
|
|
||||||
|
SignerList mainSignerList_;
|
||||||
|
SignerList sideSignerList_;
|
||||||
|
SignatureCollector mainSigCollector_;
|
||||||
|
SignatureCollector sideSigCollector_;
|
||||||
|
TicketRunner ticketRunner_;
|
||||||
|
DoorKeeper mainDoorKeeper_;
|
||||||
|
DoorKeeper sideDoorKeeper_;
|
||||||
|
|
||||||
|
boost::posix_time::seconds const heartbeatInterval{5};
|
||||||
|
std::unique_ptr<boost::asio::deadline_timer> heartbeatTimer;
|
||||||
|
|
||||||
|
struct PeerTxnSignature
|
||||||
|
{
|
||||||
|
Buffer sig;
|
||||||
|
std::uint32_t seq;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SequenceInfo
|
||||||
|
{
|
||||||
|
// Number of signatures at this sequence number
|
||||||
|
std::uint32_t count{0};
|
||||||
|
// Serialization of the transaction for everything except the signature
|
||||||
|
// id (which varies for each signature). This can be used to verify one
|
||||||
|
// of the signatures in a multisig.
|
||||||
|
Blob partialTxnSerialization;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PendingTransaction
|
||||||
|
{
|
||||||
|
STAmount amount;
|
||||||
|
AccountID srcChainSrcAccount;
|
||||||
|
AccountID dstChainDstAccount;
|
||||||
|
// Key is the federator's public key
|
||||||
|
hash_map<PublicKey, PeerTxnSignature> sigs;
|
||||||
|
// Key is a sequence number
|
||||||
|
hash_map<std::uint32_t, SequenceInfo> sequenceInfo;
|
||||||
|
// True if the transaction was ever put into the toSendTxns_ queue
|
||||||
|
bool queuedToSend_{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Key is the hash of the triggering transaction
|
||||||
|
mutable std::mutex pendingTxnsM_;
|
||||||
|
hash_map<uint256, PendingTransaction> GUARDED_BY(pendingTxnsM_)
|
||||||
|
pendingTxns_[numChains];
|
||||||
|
|
||||||
|
mutable std::mutex toSendTxnsM_;
|
||||||
|
// Signed transactions ready to send
|
||||||
|
// Key is the transaction's sequence number. The transactions must be sent
|
||||||
|
// in the correct order. If the next trasnaction the account needs to send
|
||||||
|
// has a sequence number of N, the transaction with sequence N+1 can't be
|
||||||
|
// sent just because it collected signatures first.
|
||||||
|
std::map<std::uint32_t, STTx> GUARDED_BY(toSendTxnsM_)
|
||||||
|
toSendTxns_[numChains];
|
||||||
|
std::set<std::uint32_t> GUARDED_BY(toSendTxnsM_) toSkipSeq_[numChains];
|
||||||
|
|
||||||
|
// Use a condition variable to prevent busy waiting when the queue is
|
||||||
|
// empty
|
||||||
|
mutable std::mutex m_;
|
||||||
|
std::condition_variable cv_;
|
||||||
|
|
||||||
|
// prevent the main loop from starting until explictly told to run.
|
||||||
|
// This is used to allow bootstrap code to run before any events are
|
||||||
|
// processed
|
||||||
|
mutable std::mutex mainLoopMutex_;
|
||||||
|
bool mainLoopLocked_{true};
|
||||||
|
std::condition_variable mainLoopCv_;
|
||||||
|
beast::Journal j_;
|
||||||
|
|
||||||
|
static std::array<
|
||||||
|
boost::container::flat_map<Issue, OtherChainAssetProperties>,
|
||||||
|
numChains>
|
||||||
|
makeAssetProps(BasicConfig const& config, beast::Journal j);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Constructor should be private, but needs to be public so
|
||||||
|
// `make_shared` can use it
|
||||||
|
Federator(
|
||||||
|
PrivateTag,
|
||||||
|
Application& app,
|
||||||
|
SecretKey signingKey,
|
||||||
|
hash_set<PublicKey>&& federators,
|
||||||
|
boost::asio::ip::address mainChainIp,
|
||||||
|
std::uint16_t mainChainPort,
|
||||||
|
AccountID const& mainAccount,
|
||||||
|
AccountID const& sideAccount,
|
||||||
|
std::array<
|
||||||
|
boost::container::flat_map<Issue, OtherChainAssetProperties>,
|
||||||
|
numChains>&& assetProps,
|
||||||
|
beast::Journal j);
|
||||||
|
|
||||||
|
~Federator();
|
||||||
|
|
||||||
|
void
|
||||||
|
start();
|
||||||
|
|
||||||
|
void
|
||||||
|
stop() EXCLUDES(m_);
|
||||||
|
|
||||||
|
void
|
||||||
|
push(FederatorEvent&& e) EXCLUDES(m_, eventsMutex_);
|
||||||
|
|
||||||
|
// Don't process any events until the bootstrap has a chance to run
|
||||||
|
void
|
||||||
|
unlockMainLoop(UnlockMainLoopKey key) EXCLUDES(m_);
|
||||||
|
|
||||||
|
void
|
||||||
|
addPendingTxnSig(
|
||||||
|
TxnType txnType,
|
||||||
|
ChainType chaintype,
|
||||||
|
PublicKey const& federatorPk,
|
||||||
|
uint256 const& srcChainTxnHash,
|
||||||
|
std::optional<uint256> const& dstChainTxnHash,
|
||||||
|
STAmount const& amt,
|
||||||
|
AccountID const& srcChainSrcAccount,
|
||||||
|
AccountID const& dstChainDstAccount,
|
||||||
|
std::uint32_t seq,
|
||||||
|
Buffer&& sig) EXCLUDES(federatorPKsMutex_, pendingTxnsM_, toSendTxnsM_);
|
||||||
|
|
||||||
|
void
|
||||||
|
addPendingTxnSig(
|
||||||
|
ChainType chaintype,
|
||||||
|
PublicKey const& publicKey,
|
||||||
|
uint256 const& mId,
|
||||||
|
Buffer&& sig);
|
||||||
|
|
||||||
|
// Return true if a transaction with this sequence has already been sent
|
||||||
|
bool
|
||||||
|
alreadySent(ChainType chaintype, std::uint32_t seq) const;
|
||||||
|
|
||||||
|
void
|
||||||
|
setLastXChainTxnWithResult(
|
||||||
|
ChainType chaintype,
|
||||||
|
std::uint32_t seq,
|
||||||
|
std::uint32_t seqTook,
|
||||||
|
uint256 const& hash);
|
||||||
|
void
|
||||||
|
setNoLastXChainTxnWithResult(ChainType chaintype);
|
||||||
|
void
|
||||||
|
stopHistoricalTxns(ChainType chaintype);
|
||||||
|
void
|
||||||
|
initialSyncDone(ChainType chaintype);
|
||||||
|
|
||||||
|
// Get stats on the federator, including pending transactions and
|
||||||
|
// initialization state
|
||||||
|
Json::Value
|
||||||
|
getInfo() const EXCLUDES(pendingTxnsM_);
|
||||||
|
|
||||||
|
void
|
||||||
|
sweep();
|
||||||
|
|
||||||
|
SignatureCollector&
|
||||||
|
getSignatureCollector(ChainType chain);
|
||||||
|
DoorKeeper&
|
||||||
|
getDoorKeeper(ChainType chain);
|
||||||
|
TicketRunner&
|
||||||
|
getTicketRunner();
|
||||||
|
void
|
||||||
|
addSeqToSkip(ChainType chain, std::uint32_t seq) EXCLUDES(toSendTxnsM_);
|
||||||
|
// TODO multi-sig refactor?
|
||||||
|
void
|
||||||
|
addTxToSend(ChainType chain, std::uint32_t seq, STTx const& tx)
|
||||||
|
EXCLUDES(toSendTxnsM_);
|
||||||
|
|
||||||
|
// Set the accountSeq to the max of the current value and the requested
|
||||||
|
// value. This is done with a lock free algorithm.
|
||||||
|
void
|
||||||
|
setAccountSeqMax(ChainType chaintype, std::uint32_t reqValue);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Two phase init needed for shared_from this.
|
||||||
|
// Only called from `make_Federator`
|
||||||
|
void
|
||||||
|
init(
|
||||||
|
boost::asio::io_service& ios,
|
||||||
|
boost::asio::ip::address& ip,
|
||||||
|
std::uint16_t port,
|
||||||
|
std::shared_ptr<MainchainListener>&& mainchainListener,
|
||||||
|
std::shared_ptr<SidechainListener>&& sidechainListener);
|
||||||
|
|
||||||
|
void
|
||||||
|
heartbeatTimerHandler(const boost::system::error_code& ec);
|
||||||
|
|
||||||
|
// Convert between the asset on the src chain to the asset on the other
|
||||||
|
// chain. The `assetProps_` array controls how this conversion is done.
|
||||||
|
// An empty option is returned if the from issue is not part of the map in
|
||||||
|
// the `assetProps_` array.
|
||||||
|
[[nodiscard]] std::optional<STAmount>
|
||||||
|
toOtherChainAmount(ChainType srcChain, STAmount const& from) const;
|
||||||
|
|
||||||
|
// Set the lastTxnSeqSent to the max of the current value and the requested
|
||||||
|
// value. This is done with a lock free algorithm.
|
||||||
|
void
|
||||||
|
setLastTxnSeqSentMax(ChainType chaintype, std::uint32_t reqValue);
|
||||||
|
void
|
||||||
|
setLastTxnSeqConfirmedMax(ChainType chaintype, std::uint32_t reqValue);
|
||||||
|
|
||||||
|
mutable std::mutex sendTxnsMutex_;
|
||||||
|
void
|
||||||
|
sendTxns() EXCLUDES(sendTxnsMutex_, toSendTxnsM_);
|
||||||
|
|
||||||
|
void
|
||||||
|
mainLoop() EXCLUDES(mainLoopMutex_);
|
||||||
|
|
||||||
|
void
|
||||||
|
payTxn(
|
||||||
|
TxnType txnType,
|
||||||
|
ChainType dstChain,
|
||||||
|
STAmount const& amt,
|
||||||
|
// srcChainSrcAccount is the origional sending account in a cross chain
|
||||||
|
// transaction. Note, for refunds, the srcChainSrcAccount and the dst
|
||||||
|
// will be the same.
|
||||||
|
AccountID const& srcChainSrcAccount,
|
||||||
|
AccountID const& dst,
|
||||||
|
uint256 const& srcChainTxnHash,
|
||||||
|
std::optional<uint256> const& dstChainTxnHash);
|
||||||
|
|
||||||
|
// Issue a refund to the destination account. Refunds may be issued when a
|
||||||
|
// cross chain transaction fails on the destination chain. In this case, the
|
||||||
|
// funds will already be locked on one chain, but can not be completed on
|
||||||
|
// the other chain. Note that refunds may not be for the full amount sent.
|
||||||
|
// In effect, not refunding the full amount charges a fee to discourage
|
||||||
|
// abusing refunds to try to overload the system.
|
||||||
|
void
|
||||||
|
sendRefund(
|
||||||
|
ChainType chaintype,
|
||||||
|
STAmount const& amt,
|
||||||
|
AccountID const& dst,
|
||||||
|
uint256 const& txnHash,
|
||||||
|
uint256 const& triggeringResultTxnHash);
|
||||||
|
|
||||||
|
void
|
||||||
|
onEvent(event::XChainTransferDetected const& e);
|
||||||
|
void
|
||||||
|
onEvent(event::XChainTransferResult const& e) EXCLUDES(pendingTxnsM_);
|
||||||
|
void
|
||||||
|
onEvent(event::RefundTransferResult const& e) EXCLUDES(pendingTxnsM_);
|
||||||
|
void
|
||||||
|
onEvent(event::HeartbeatTimer const& e);
|
||||||
|
void
|
||||||
|
onEvent(event::TicketCreateTrigger const& e);
|
||||||
|
void
|
||||||
|
onEvent(event::TicketCreateResult const& e);
|
||||||
|
void
|
||||||
|
onEvent(event::DepositAuthResult const& e);
|
||||||
|
void
|
||||||
|
onEvent(event::BootstrapTicket const& e);
|
||||||
|
void
|
||||||
|
onEvent(event::DisableMasterKeyResult const& e);
|
||||||
|
// void
|
||||||
|
// onEvent(event::SignerListSetResult const& e);
|
||||||
|
|
||||||
|
void
|
||||||
|
updateDoorKeeper(ChainType chainType) EXCLUDES(pendingTxnsM_);
|
||||||
|
void
|
||||||
|
onResult(ChainType chainType, std::uint32_t resultTxSeq);
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] std::shared_ptr<Federator>
|
||||||
|
make_Federator(
|
||||||
|
Application& app,
|
||||||
|
boost::asio::io_service& ios,
|
||||||
|
BasicConfig const& config,
|
||||||
|
beast::Journal j);
|
||||||
|
|
||||||
|
// Id used for message suppression
|
||||||
|
[[nodiscard]] uint256
|
||||||
|
crossChainTxnSignatureId(
|
||||||
|
PublicKey signingPK,
|
||||||
|
uint256 const& srcChainTxnHash,
|
||||||
|
std::optional<uint256> const& dstChainTxnHash,
|
||||||
|
STAmount const& amt,
|
||||||
|
AccountID const& src,
|
||||||
|
AccountID const& dst,
|
||||||
|
std::uint32_t seq,
|
||||||
|
Slice const& signature);
|
||||||
|
|
||||||
|
[[nodiscard]] Federator::ChainType
|
||||||
|
srcChainType(event::Dir dir);
|
||||||
|
|
||||||
|
[[nodiscard]] Federator::ChainType
|
||||||
|
dstChainType(event::Dir dir);
|
||||||
|
|
||||||
|
[[nodiscard]] Federator::ChainType
|
||||||
|
otherChainType(Federator::ChainType ct);
|
||||||
|
|
||||||
|
[[nodiscard]] Federator::ChainType
|
||||||
|
getChainType(bool isMainchain);
|
||||||
|
|
||||||
|
uint256
|
||||||
|
computeMessageSuppression(uint256 const& mId, Slice const& signature);
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
|
|
||||||
|
#endif
|
||||||
325
src/ripple/app/sidechain/FederatorEvents.cpp
Normal file
325
src/ripple/app/sidechain/FederatorEvents.cpp
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/FederatorEvents.h>
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
namespace event {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string const&
|
||||||
|
to_string(Dir dir)
|
||||||
|
{
|
||||||
|
switch (dir)
|
||||||
|
{
|
||||||
|
case Dir::mainToSide: {
|
||||||
|
static std::string const r("main");
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
case Dir::sideToMain: {
|
||||||
|
static std::string const r("side");
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some compilers will warn about not returning from all control paths
|
||||||
|
// without this, but this code will never execute.
|
||||||
|
assert(0);
|
||||||
|
static std::string const error("error");
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string const&
|
||||||
|
to_string(AccountFlagOp op)
|
||||||
|
{
|
||||||
|
switch (op)
|
||||||
|
{
|
||||||
|
case AccountFlagOp::set: {
|
||||||
|
static std::string const r("set");
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
case AccountFlagOp::clear: {
|
||||||
|
static std::string const r("clear");
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some compilers will warn about not returning from all control paths
|
||||||
|
// without this, but this code will never execute.
|
||||||
|
assert(0);
|
||||||
|
static std::string const error("error");
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
EventType
|
||||||
|
XChainTransferDetected::eventType() const
|
||||||
|
{
|
||||||
|
return EventType::trigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
XChainTransferDetected::toJson() const
|
||||||
|
{
|
||||||
|
Json::Value result{Json::objectValue};
|
||||||
|
result["eventType"] = "XChainTransferDetected";
|
||||||
|
result["src"] = toBase58(src_);
|
||||||
|
result["dst"] = toBase58(dst_);
|
||||||
|
result["deliveredAmt"] = deliveredAmt_.getJson(JsonOptions::none);
|
||||||
|
result["txnSeq"] = txnSeq_;
|
||||||
|
result["txnHash"] = to_string(txnHash_);
|
||||||
|
result["rpcOrder"] = rpcOrder_;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventType
|
||||||
|
HeartbeatTimer::eventType() const
|
||||||
|
{
|
||||||
|
return EventType::heartbeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
HeartbeatTimer::toJson() const
|
||||||
|
{
|
||||||
|
Json::Value result{Json::objectValue};
|
||||||
|
result["eventType"] = "HeartbeatTimer";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventType
|
||||||
|
XChainTransferResult::eventType() const
|
||||||
|
{
|
||||||
|
return EventType::result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
XChainTransferResult::toJson() const
|
||||||
|
{
|
||||||
|
Json::Value result{Json::objectValue};
|
||||||
|
result["eventType"] = "XChainTransferResult";
|
||||||
|
result["dir"] = to_string(dir_);
|
||||||
|
result["dst"] = toBase58(dst_);
|
||||||
|
if (deliveredAmt_)
|
||||||
|
result["deliveredAmt"] = deliveredAmt_->getJson(JsonOptions::none);
|
||||||
|
result["txnSeq"] = txnSeq_;
|
||||||
|
result["srcChainTxnHash"] = to_string(srcChainTxnHash_);
|
||||||
|
result["txnHash"] = to_string(txnHash_);
|
||||||
|
result["ter"] = transHuman(ter_);
|
||||||
|
result["rpcOrder"] = rpcOrder_;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventType
|
||||||
|
RefundTransferResult::eventType() const
|
||||||
|
{
|
||||||
|
return EventType::result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
RefundTransferResult::toJson() const
|
||||||
|
{
|
||||||
|
Json::Value result{Json::objectValue};
|
||||||
|
result["eventType"] = "RefundTransferResult";
|
||||||
|
result["dir"] = to_string(dir_);
|
||||||
|
result["dst"] = toBase58(dst_);
|
||||||
|
if (deliveredAmt_)
|
||||||
|
result["deliveredAmt"] = deliveredAmt_->getJson(JsonOptions::none);
|
||||||
|
result["txnSeq"] = txnSeq_;
|
||||||
|
result["srcChainTxnHash"] = to_string(srcChainTxnHash_);
|
||||||
|
result["dstChainTxnHash"] = to_string(dstChainTxnHash_);
|
||||||
|
result["txnHash"] = to_string(txnHash_);
|
||||||
|
result["ter"] = transHuman(ter_);
|
||||||
|
result["rpcOrder"] = rpcOrder_;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventType
|
||||||
|
TicketCreateTrigger::eventType() const
|
||||||
|
{
|
||||||
|
return EventType::trigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
TicketCreateTrigger::toJson() const
|
||||||
|
{
|
||||||
|
Json::Value result{Json::objectValue};
|
||||||
|
result["eventType"] = "TicketCreateTrigger";
|
||||||
|
result["dir"] = to_string(dir_);
|
||||||
|
result["success"] = success_;
|
||||||
|
result["txnSeq"] = txnSeq_;
|
||||||
|
result["ledgerIndex"] = ledgerIndex_;
|
||||||
|
result["txnHash"] = to_string(txnHash_);
|
||||||
|
result["rpcOrder"] = rpcOrder_;
|
||||||
|
result["sourceTag"] = sourceTag_;
|
||||||
|
result["memo"] = memoStr_;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventType
|
||||||
|
TicketCreateResult::eventType() const
|
||||||
|
{
|
||||||
|
return EventType::resultAndTrigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
TicketCreateResult::toJson() const
|
||||||
|
{
|
||||||
|
Json::Value result{Json::objectValue};
|
||||||
|
result["eventType"] = "TicketCreateResult";
|
||||||
|
result["dir"] = to_string(dir_);
|
||||||
|
result["success"] = success_;
|
||||||
|
result["txnSeq"] = txnSeq_;
|
||||||
|
result["ledgerIndex"] = ledgerIndex_;
|
||||||
|
result["srcChainTxnHash"] = to_string(srcChainTxnHash_);
|
||||||
|
result["txnHash"] = to_string(txnHash_);
|
||||||
|
result["rpcOrder"] = rpcOrder_;
|
||||||
|
result["sourceTag"] = sourceTag_;
|
||||||
|
result["memo"] = memoStr_;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketCreateResult::removeTrigger()
|
||||||
|
{
|
||||||
|
memoStr_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
EventType
|
||||||
|
DepositAuthResult::eventType() const
|
||||||
|
{
|
||||||
|
return EventType::result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
DepositAuthResult::toJson() const
|
||||||
|
{
|
||||||
|
Json::Value result{Json::objectValue};
|
||||||
|
result["eventType"] = "DepositAuthResult";
|
||||||
|
result["dir"] = to_string(dir_);
|
||||||
|
result["success"] = success_;
|
||||||
|
result["txnSeq"] = txnSeq_;
|
||||||
|
result["ledgerIndex"] = ledgerIndex_;
|
||||||
|
result["srcChainTxnHash"] = to_string(srcChainTxnHash_);
|
||||||
|
result["rpcOrder"] = rpcOrder_;
|
||||||
|
result["op"] = to_string(op_);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventType
|
||||||
|
SignerListSetResult::eventType() const
|
||||||
|
{
|
||||||
|
return EventType::result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
SignerListSetResult::toJson() const
|
||||||
|
{
|
||||||
|
Json::Value result{Json::objectValue};
|
||||||
|
result["eventType"] = "SignerListSetResult";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
EventType
|
||||||
|
BootstrapTicket::eventType() const
|
||||||
|
{
|
||||||
|
return EventType::bootstrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
BootstrapTicket::toJson() const
|
||||||
|
{
|
||||||
|
Json::Value result{Json::objectValue};
|
||||||
|
result["eventType"] = "BootstrapTicket";
|
||||||
|
result["isMainchain"] = isMainchain_;
|
||||||
|
result["txnSeq"] = txnSeq_;
|
||||||
|
result["rpcOrder"] = rpcOrder_;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
EventType
|
||||||
|
DisableMasterKeyResult::eventType() const
|
||||||
|
{
|
||||||
|
return EventType::result; // TODO change to bootstrap type too?
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
DisableMasterKeyResult::toJson() const
|
||||||
|
{
|
||||||
|
Json::Value result{Json::objectValue};
|
||||||
|
result["eventType"] = "DisableMasterKeyResult";
|
||||||
|
result["isMainchain"] = isMainchain_;
|
||||||
|
result["txnSeq"] = txnSeq_;
|
||||||
|
result["rpcOrder"] = rpcOrder_;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace event
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
template <typename T, typename = void>
|
||||||
|
struct hasTxnHash : std::false_type
|
||||||
|
{
|
||||||
|
};
|
||||||
|
template <typename T>
|
||||||
|
struct hasTxnHash<T, std::void_t<decltype(std::declval<T>().txnHash_)>>
|
||||||
|
: std::true_type
|
||||||
|
{
|
||||||
|
};
|
||||||
|
template <class T>
|
||||||
|
inline constexpr bool hasTxnHash_v = hasTxnHash<T>::value;
|
||||||
|
|
||||||
|
// Check that the traits work as expected
|
||||||
|
static_assert(
|
||||||
|
hasTxnHash_v<event::XChainTransferResult> &&
|
||||||
|
!hasTxnHash_v<event::HeartbeatTimer>,
|
||||||
|
"");
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::optional<uint256>
|
||||||
|
txnHash(FederatorEvent const& event)
|
||||||
|
{
|
||||||
|
return std::visit(
|
||||||
|
[](auto const& e) -> std::optional<uint256> {
|
||||||
|
if constexpr (hasTxnHash_v<std::decay_t<decltype(e)>>)
|
||||||
|
{
|
||||||
|
return e.txnHash_;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
},
|
||||||
|
event);
|
||||||
|
}
|
||||||
|
|
||||||
|
event::EventType
|
||||||
|
eventType(FederatorEvent const& event)
|
||||||
|
{
|
||||||
|
return std::visit([](auto const& e) { return e.eventType(); }, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson(FederatorEvent const& event)
|
||||||
|
{
|
||||||
|
return std::visit([](auto const& e) { return e.toJson(); }, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
262
src/ripple/app/sidechain/FederatorEvents.h
Normal file
262
src/ripple/app/sidechain/FederatorEvents.h
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#ifndef RIPPLE_SIDECHAIN_FEDERATOR_EVENTS_H_INCLUDED
|
||||||
|
#define RIPPLE_SIDECHAIN_FEDERATOR_EVENTS_H_INCLUDED
|
||||||
|
|
||||||
|
#include <ripple/beast/utility/Journal.h>
|
||||||
|
#include <ripple/json/json_value.h>
|
||||||
|
#include <ripple/protocol/AccountID.h>
|
||||||
|
#include <ripple/protocol/Issue.h>
|
||||||
|
#include <ripple/protocol/STAmount.h>
|
||||||
|
#include <ripple/protocol/TER.h>
|
||||||
|
|
||||||
|
#include <boost/format.hpp>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
namespace event {
|
||||||
|
|
||||||
|
enum class Dir { sideToMain, mainToSide };
|
||||||
|
enum class AccountFlagOp { set, clear };
|
||||||
|
static constexpr std::uint32_t MemoStringMax = 512;
|
||||||
|
|
||||||
|
enum class EventType {
|
||||||
|
bootstrap,
|
||||||
|
trigger,
|
||||||
|
result,
|
||||||
|
resultAndTrigger,
|
||||||
|
heartbeat,
|
||||||
|
};
|
||||||
|
|
||||||
|
// A cross chain transfer was detected on this federator
|
||||||
|
struct XChainTransferDetected
|
||||||
|
{
|
||||||
|
// direction of the transfer
|
||||||
|
Dir dir_;
|
||||||
|
// Src account on the src chain
|
||||||
|
AccountID src_;
|
||||||
|
// Dst account on the dst chain
|
||||||
|
AccountID dst_;
|
||||||
|
STAmount deliveredAmt_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
uint256 txnHash_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HeartbeatTimer
|
||||||
|
{
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct XChainTransferResult
|
||||||
|
{
|
||||||
|
// direction is the direction of the triggering transaction.
|
||||||
|
// I.e. A "mainToSide" transfer result is a transaction that
|
||||||
|
// happens on the sidechain (the triggering transaction happended on the
|
||||||
|
// mainchain)
|
||||||
|
Dir dir_;
|
||||||
|
AccountID dst_;
|
||||||
|
std::optional<STAmount> deliveredAmt_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
// Txn hash of the initiating xchain transaction
|
||||||
|
uint256 srcChainTxnHash_;
|
||||||
|
// Txn has of the federator's transaction on the dst chain
|
||||||
|
uint256 txnHash_;
|
||||||
|
TER ter_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RefundTransferResult
|
||||||
|
{
|
||||||
|
// direction is the direction of the triggering transaction.
|
||||||
|
// I.e. A "mainToSide" refund transfer result is a transaction that
|
||||||
|
// happens on the mainchain (the triggering transaction happended on the
|
||||||
|
// mainchain, the failed result happened on the side chain, and the refund
|
||||||
|
// result happened on the mainchain)
|
||||||
|
Dir dir_;
|
||||||
|
AccountID dst_;
|
||||||
|
std::optional<STAmount> deliveredAmt_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
// Txn hash of the initiating xchain transaction
|
||||||
|
uint256 srcChainTxnHash_;
|
||||||
|
// Txn hash of the federator's transaction on the dst chain
|
||||||
|
uint256 dstChainTxnHash_;
|
||||||
|
// Txn hash of the refund result
|
||||||
|
uint256 txnHash_;
|
||||||
|
TER ter_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TicketCreateTrigger
|
||||||
|
{
|
||||||
|
Dir dir_;
|
||||||
|
bool success_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
std::uint32_t ledgerIndex_;
|
||||||
|
uint256 txnHash_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
std::uint32_t sourceTag_;
|
||||||
|
std::string memoStr_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TicketCreateResult
|
||||||
|
{
|
||||||
|
Dir dir_;
|
||||||
|
bool success_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
std::uint32_t ledgerIndex_;
|
||||||
|
uint256 srcChainTxnHash_;
|
||||||
|
uint256 txnHash_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
std::uint32_t sourceTag_;
|
||||||
|
std::string memoStr_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
|
||||||
|
void
|
||||||
|
removeTrigger();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DepositAuthResult
|
||||||
|
{
|
||||||
|
Dir dir_;
|
||||||
|
bool success_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
std::uint32_t ledgerIndex_;
|
||||||
|
uint256 srcChainTxnHash_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
AccountFlagOp op_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SignerListSetResult
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BootstrapTicket
|
||||||
|
{
|
||||||
|
bool isMainchain_;
|
||||||
|
bool success_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
std::uint32_t ledgerIndex_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
std::uint32_t sourceTag_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DisableMasterKeyResult
|
||||||
|
{
|
||||||
|
bool isMainchain_;
|
||||||
|
std::uint32_t txnSeq_;
|
||||||
|
std::int32_t rpcOrder_;
|
||||||
|
|
||||||
|
EventType
|
||||||
|
eventType() const;
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace event
|
||||||
|
|
||||||
|
using FederatorEvent = std::variant<
|
||||||
|
event::XChainTransferDetected,
|
||||||
|
event::HeartbeatTimer,
|
||||||
|
event::XChainTransferResult,
|
||||||
|
event::RefundTransferResult,
|
||||||
|
event::TicketCreateTrigger,
|
||||||
|
event::TicketCreateResult,
|
||||||
|
event::DepositAuthResult,
|
||||||
|
event::BootstrapTicket,
|
||||||
|
event::DisableMasterKeyResult>;
|
||||||
|
|
||||||
|
event::EventType
|
||||||
|
eventType(FederatorEvent const& event);
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
toJson(FederatorEvent const& event);
|
||||||
|
|
||||||
|
// If the event has a txnHash_ field (all the trigger events), return the hash,
|
||||||
|
// otherwise return nullopt
|
||||||
|
std::optional<uint256>
|
||||||
|
txnHash(FederatorEvent const& event);
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
|
|
||||||
|
#endif
|
||||||
856
src/ripple/app/sidechain/impl/ChainListener.cpp
Normal file
856
src/ripple/app/sidechain/impl/ChainListener.cpp
Normal file
@@ -0,0 +1,856 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/impl/ChainListener.h>
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/Federator.h>
|
||||||
|
#include <ripple/app/sidechain/FederatorEvents.h>
|
||||||
|
#include <ripple/app/sidechain/impl/WebsocketClient.h>
|
||||||
|
#include <ripple/basics/Log.h>
|
||||||
|
#include <ripple/basics/XRPAmount.h>
|
||||||
|
#include <ripple/basics/strHex.h>
|
||||||
|
#include <ripple/json/Output.h>
|
||||||
|
#include <ripple/json/json_writer.h>
|
||||||
|
#include <ripple/protocol/AccountID.h>
|
||||||
|
#include <ripple/protocol/SField.h>
|
||||||
|
#include <ripple/protocol/STAmount.h>
|
||||||
|
#include <ripple/protocol/TxFlags.h>
|
||||||
|
#include <ripple/protocol/jss.h>
|
||||||
|
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
class Federator;
|
||||||
|
|
||||||
|
ChainListener::ChainListener(
|
||||||
|
IsMainchain isMainchain,
|
||||||
|
AccountID const& account,
|
||||||
|
std::weak_ptr<Federator>&& federator,
|
||||||
|
beast::Journal j)
|
||||||
|
: isMainchain_{isMainchain == IsMainchain::yes}
|
||||||
|
, doorAccount_{account}
|
||||||
|
, doorAccountStr_{toBase58(account)}
|
||||||
|
, federator_{std::move(federator)}
|
||||||
|
, initialSync_{std::make_unique<InitialSync>(federator_, isMainchain_, j)}
|
||||||
|
, j_{j}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// destructor must be defined after WebsocketClient size is known (i.e. it can
|
||||||
|
// not be defaulted in the header or the unique_ptr declration of
|
||||||
|
// WebsocketClient won't work)
|
||||||
|
ChainListener::~ChainListener() = default;
|
||||||
|
|
||||||
|
std::string const&
|
||||||
|
ChainListener::chainName() const
|
||||||
|
{
|
||||||
|
// Note: If this function is ever changed to return a value instead of a
|
||||||
|
// ref, review the code to ensure the "jv" functions don't bind to temps
|
||||||
|
static const std::string m("Mainchain");
|
||||||
|
static const std::string s("Sidechain");
|
||||||
|
return isMainchain_ ? m : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
template <class T>
|
||||||
|
std::optional<T>
|
||||||
|
getMemoData(Json::Value const& v, std::uint32_t index) = delete;
|
||||||
|
|
||||||
|
template <>
|
||||||
|
std::optional<uint256>
|
||||||
|
getMemoData<uint256>(Json::Value const& v, std::uint32_t index)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
uint256 result;
|
||||||
|
if (result.parseHex(
|
||||||
|
v[jss::Memos][index][jss::Memo][jss::MemoData].asString()))
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
std::optional<uint8_t>
|
||||||
|
getMemoData<uint8_t>(Json::Value const& v, std::uint32_t index)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
auto const hexData =
|
||||||
|
v[jss::Memos][index][jss::Memo][jss::MemoData].asString();
|
||||||
|
auto d = hexData.data();
|
||||||
|
if (hexData.size() != 2)
|
||||||
|
return {};
|
||||||
|
auto highNibble = charUnHex(d[0]);
|
||||||
|
auto lowNibble = charUnHex(d[1]);
|
||||||
|
if (highNibble < 0 || lowNibble < 0)
|
||||||
|
return {};
|
||||||
|
return (highNibble << 4) | lowNibble;
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
template <class E>
|
||||||
|
void
|
||||||
|
ChainListener::pushEvent(
|
||||||
|
E&& e,
|
||||||
|
int txHistoryIndex,
|
||||||
|
std::lock_guard<std::mutex> const&)
|
||||||
|
{
|
||||||
|
static_assert(std::is_rvalue_reference_v<decltype(e)>, "");
|
||||||
|
|
||||||
|
if (initialSync_)
|
||||||
|
{
|
||||||
|
auto const hasReplayed = initialSync_->onEvent(std::move(e));
|
||||||
|
if (hasReplayed)
|
||||||
|
initialSync_.reset();
|
||||||
|
}
|
||||||
|
else if (auto f = federator_.lock(); f && txHistoryIndex >= 0)
|
||||||
|
{
|
||||||
|
f->push(std::move(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ChainListener::processMessage(Json::Value const& msg)
|
||||||
|
{
|
||||||
|
// Even though this lock has a large scope, this function does very little
|
||||||
|
// processing and should run relatively quickly
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"chain listener message",
|
||||||
|
jv("msg", msg),
|
||||||
|
jv("isMainchain", isMainchain_));
|
||||||
|
|
||||||
|
if (!msg.isMember(jss::validated) || !msg[jss::validated].asBool())
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"ignoring listener message",
|
||||||
|
jv("reason", "not validated"),
|
||||||
|
jv("msg", msg),
|
||||||
|
jv("chain_name", chainName()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!msg.isMember(jss::engine_result_code))
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"ignoring listener message",
|
||||||
|
jv("reason", "no engine result code"),
|
||||||
|
jv("msg", msg),
|
||||||
|
jv("chain_name", chainName()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!msg.isMember(jss::account_history_tx_index))
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"ignoring listener message",
|
||||||
|
jv("reason", "no account history tx index"),
|
||||||
|
jv("msg", msg),
|
||||||
|
jv("chain_name", chainName()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!msg.isMember(jss::meta))
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"ignoring listener message",
|
||||||
|
jv("reason", "tx meta"),
|
||||||
|
jv("msg", msg),
|
||||||
|
jv("chain_name", chainName()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto fieldMatchesStr =
|
||||||
|
[](Json::Value const& val, char const* field, char const* toMatch) {
|
||||||
|
if (!val.isMember(field))
|
||||||
|
return false;
|
||||||
|
auto const f = val[field];
|
||||||
|
if (!f.isString())
|
||||||
|
return false;
|
||||||
|
return f.asString() == toMatch;
|
||||||
|
};
|
||||||
|
|
||||||
|
TER const txnTER = [&msg] {
|
||||||
|
return TER::fromInt(msg[jss::engine_result_code].asInt());
|
||||||
|
}();
|
||||||
|
|
||||||
|
bool const txnSuccess = (txnTER == tesSUCCESS);
|
||||||
|
|
||||||
|
// values < 0 are historical txns. values >= 0 are new transactions. Only
|
||||||
|
// the initial sync needs historical txns.
|
||||||
|
int const txnHistoryIndex = msg[jss::account_history_tx_index].asInt();
|
||||||
|
|
||||||
|
auto const meta = msg[jss::meta];
|
||||||
|
|
||||||
|
// There are two payment types of interest:
|
||||||
|
// 1. User initiated payments on this chain that trigger a transaction on
|
||||||
|
// the other chain.
|
||||||
|
// 2. Federated initated payments on this chain whose status needs to be
|
||||||
|
// checked.
|
||||||
|
enum class PaymentType { user, federator };
|
||||||
|
auto paymentTypeOpt = [&]() -> std::optional<PaymentType> {
|
||||||
|
// Only keep transactions to or from the door account.
|
||||||
|
// Transactions to the account are initiated by users and are are cross
|
||||||
|
// chain transactions. Transaction from the account are initiated by
|
||||||
|
// federators and need to be monitored for errors. There are two types
|
||||||
|
// of transactions that originate from the door account: the second half
|
||||||
|
// of a cross chain payment and a refund of a failed cross chain
|
||||||
|
// payment.
|
||||||
|
|
||||||
|
if (!fieldMatchesStr(msg, jss::type, jss::transaction))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
if (!msg.isMember(jss::transaction))
|
||||||
|
return {};
|
||||||
|
auto const txn = msg[jss::transaction];
|
||||||
|
|
||||||
|
if (!fieldMatchesStr(txn, jss::TransactionType, "Payment"))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
bool const accIsSrc =
|
||||||
|
fieldMatchesStr(txn, jss::Account, doorAccountStr_.c_str());
|
||||||
|
bool const accIsDst =
|
||||||
|
fieldMatchesStr(txn, jss::Destination, doorAccountStr_.c_str());
|
||||||
|
|
||||||
|
if (accIsSrc == accIsDst)
|
||||||
|
{
|
||||||
|
// either account is not involved, or self send
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accIsSrc)
|
||||||
|
return PaymentType::federator;
|
||||||
|
return PaymentType::user;
|
||||||
|
}();
|
||||||
|
|
||||||
|
// There are four types of messages used to control the federator accounts:
|
||||||
|
// 1. AccountSet without modifying account settings. These txns are used to
|
||||||
|
// trigger TicketCreate txns.
|
||||||
|
// 2. TicketCreate to issue tickets.
|
||||||
|
// 3. AccountSet that changes the depositAuth setting of accounts.
|
||||||
|
// 4. SignerListSet to update the signerList of accounts.
|
||||||
|
// 5. AccoutSet that disables the master key. All transactions before this
|
||||||
|
// are used for setup only and should be ignored. This transaction is also
|
||||||
|
// used to help set the initial transaction sequence numbers
|
||||||
|
enum class AccountControlType {
|
||||||
|
trigger,
|
||||||
|
ticket,
|
||||||
|
depositAuth,
|
||||||
|
signerList,
|
||||||
|
disableMasterKey
|
||||||
|
};
|
||||||
|
auto accountControlTypeOpt = [&]() -> std::optional<AccountControlType> {
|
||||||
|
if (!fieldMatchesStr(msg, jss::type, jss::transaction))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
if (!msg.isMember(jss::transaction))
|
||||||
|
return {};
|
||||||
|
auto const txn = msg[jss::transaction];
|
||||||
|
|
||||||
|
if (fieldMatchesStr(txn, jss::TransactionType, "AccountSet"))
|
||||||
|
{
|
||||||
|
if (!(txn.isMember(jss::SetFlag) || txn.isMember(jss::ClearFlag)))
|
||||||
|
{
|
||||||
|
return AccountControlType::trigger;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Get the flags value at the key. If the key is not present,
|
||||||
|
// return 0.
|
||||||
|
auto getFlags =
|
||||||
|
[&txn](Json::StaticString const& key) -> std::uint32_t {
|
||||||
|
if (txn.isMember(key))
|
||||||
|
{
|
||||||
|
auto const val = txn[key];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return val.asUInt();
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::uint32_t const setFlags = getFlags(jss::SetFlag);
|
||||||
|
std::uint32_t const clearFlags = getFlags(jss::ClearFlag);
|
||||||
|
|
||||||
|
if (setFlags == asfDepositAuth || clearFlags == asfDepositAuth)
|
||||||
|
return AccountControlType::depositAuth;
|
||||||
|
|
||||||
|
if (setFlags == asfDisableMaster)
|
||||||
|
return AccountControlType::disableMasterKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fieldMatchesStr(txn, jss::TransactionType, "TicketCreate"))
|
||||||
|
return AccountControlType::ticket;
|
||||||
|
if (fieldMatchesStr(txn, jss::TransactionType, "SignerListSet"))
|
||||||
|
return AccountControlType::signerList;
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (!paymentTypeOpt && !accountControlTypeOpt)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.warn(),
|
||||||
|
"ignoring listener message",
|
||||||
|
jv("reason", "wrong type, not payment nor account control tx"),
|
||||||
|
jv("msg", msg),
|
||||||
|
jv("chain_name", chainName()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert(!paymentTypeOpt || !accountControlTypeOpt);
|
||||||
|
|
||||||
|
auto const txnHash = [&]() -> std::optional<uint256> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
uint256 result;
|
||||||
|
if (result.parseHex(msg[jss::transaction][jss::hash].asString()))
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
// TODO: this is an insane input stream
|
||||||
|
// Detect and connect to another server
|
||||||
|
return {};
|
||||||
|
}();
|
||||||
|
if (!txnHash)
|
||||||
|
{
|
||||||
|
JLOG(j_.warn()) << "ignoring listener message, no tx hash";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const seq = [&]() -> std::optional<std::uint32_t> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return msg[jss::transaction][jss::Sequence].asUInt();
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
// TODO: this is an insane input stream
|
||||||
|
// Detect and connect to another server
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
if (!seq)
|
||||||
|
{
|
||||||
|
JLOG(j_.warn()) << "ignoring listener message, no tx seq";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paymentTypeOpt)
|
||||||
|
{
|
||||||
|
PaymentType const paymentType = *paymentTypeOpt;
|
||||||
|
|
||||||
|
std::optional<STAmount> deliveredAmt;
|
||||||
|
if (meta.isMember(jss::delivered_amount))
|
||||||
|
{
|
||||||
|
deliveredAmt =
|
||||||
|
amountFromJson(sfGeneric, meta[jss::delivered_amount]);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const src = [&]() -> std::optional<AccountID> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return parseBase58<AccountID>(
|
||||||
|
msg[jss::transaction][jss::Account].asString());
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
// TODO: this is an insane input stream
|
||||||
|
// Detect and connect to another server
|
||||||
|
return {};
|
||||||
|
}();
|
||||||
|
if (!src)
|
||||||
|
{
|
||||||
|
// TODO: handle the error
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const dst = [&]() -> std::optional<AccountID> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (paymentType)
|
||||||
|
{
|
||||||
|
case PaymentType::user: {
|
||||||
|
// This is the destination of the "other chain"
|
||||||
|
// transfer, which is specified as a memo.
|
||||||
|
if (!msg.isMember(jss::transaction))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// the memo data is a hex encoded version of the
|
||||||
|
// base58 encoded address. This was chosen for ease
|
||||||
|
// of encoding by clients.
|
||||||
|
auto const hexData =
|
||||||
|
msg[jss::transaction][jss::Memos][0u][jss::Memo]
|
||||||
|
[jss::MemoData]
|
||||||
|
.asString();
|
||||||
|
if ((hexData.size() > 100) || (hexData.size() % 2))
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
auto const asciiData = [&]() -> std::string {
|
||||||
|
std::string result;
|
||||||
|
result.reserve(40);
|
||||||
|
auto d = hexData.data();
|
||||||
|
for (int i = 0; i < hexData.size(); i += 2)
|
||||||
|
{
|
||||||
|
auto highNibble = charUnHex(d[i]);
|
||||||
|
auto lowNibble = charUnHex(d[i + 1]);
|
||||||
|
if (highNibble < 0 || lowNibble < 0)
|
||||||
|
return {};
|
||||||
|
char c = (highNibble << 4) | lowNibble;
|
||||||
|
result.push_back(c);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}();
|
||||||
|
return parseBase58<AccountID>(asciiData);
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
// User did not specify a destination address in a
|
||||||
|
// memo
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case PaymentType::federator:
|
||||||
|
return parseBase58<AccountID>(
|
||||||
|
msg[jss::transaction][jss::Destination].asString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
// TODO: this is an insane input stream
|
||||||
|
// Detect and connect to another server
|
||||||
|
return {};
|
||||||
|
}();
|
||||||
|
if (!dst)
|
||||||
|
{
|
||||||
|
// TODO: handle the error
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (paymentType)
|
||||||
|
{
|
||||||
|
case PaymentType::federator: {
|
||||||
|
auto s = txnSuccess ? j_.trace() : j_.error();
|
||||||
|
char const* status = txnSuccess ? "success" : "fail";
|
||||||
|
JLOGV(
|
||||||
|
s,
|
||||||
|
"federator txn status",
|
||||||
|
jv("chain_name", chainName()),
|
||||||
|
jv("status", status),
|
||||||
|
jv("msg", msg));
|
||||||
|
|
||||||
|
auto const txnTypeRaw =
|
||||||
|
detail::getMemoData<uint8_t>(msg[jss::transaction], 0);
|
||||||
|
|
||||||
|
if (!txnTypeRaw || *txnTypeRaw > Federator::txnTypeLast)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.fatal(),
|
||||||
|
"expected valid txnType in ChainListener",
|
||||||
|
jv("msg", msg));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Federator::TxnType const txnType =
|
||||||
|
static_cast<Federator::TxnType>(*txnTypeRaw);
|
||||||
|
|
||||||
|
auto const srcChainTxnHash =
|
||||||
|
detail::getMemoData<uint256>(msg[jss::transaction], 1);
|
||||||
|
|
||||||
|
if (!srcChainTxnHash)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.fatal(),
|
||||||
|
"expected srcChainTxnHash in ChainListener",
|
||||||
|
jv("msg", msg));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
static_assert(
|
||||||
|
Federator::txnTypeLast == 2, "Add new case below");
|
||||||
|
switch (txnType)
|
||||||
|
{
|
||||||
|
case Federator::TxnType::xChain: {
|
||||||
|
using namespace event;
|
||||||
|
// The dirction looks backwards, but it's not. The
|
||||||
|
// direction is for the *triggering* transaction.
|
||||||
|
auto const dir =
|
||||||
|
isMainchain_ ? Dir::sideToMain : Dir::mainToSide;
|
||||||
|
XChainTransferResult e{
|
||||||
|
dir,
|
||||||
|
*dst,
|
||||||
|
deliveredAmt,
|
||||||
|
*seq,
|
||||||
|
*srcChainTxnHash,
|
||||||
|
*txnHash,
|
||||||
|
txnTER,
|
||||||
|
txnHistoryIndex};
|
||||||
|
pushEvent(std::move(e), txnHistoryIndex, l);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Federator::TxnType::refund: {
|
||||||
|
using namespace event;
|
||||||
|
// The direction is for the triggering transaction.
|
||||||
|
auto const dir =
|
||||||
|
isMainchain_ ? Dir::mainToSide : Dir::sideToMain;
|
||||||
|
auto const dstChainTxnHash =
|
||||||
|
detail::getMemoData<uint256>(
|
||||||
|
msg[jss::transaction], 2);
|
||||||
|
if (!dstChainTxnHash)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.fatal(),
|
||||||
|
"expected valid dstChainTxnHash in "
|
||||||
|
"ChainListener",
|
||||||
|
jv("msg", msg));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RefundTransferResult e{
|
||||||
|
dir,
|
||||||
|
*dst,
|
||||||
|
deliveredAmt,
|
||||||
|
*seq,
|
||||||
|
*srcChainTxnHash,
|
||||||
|
*dstChainTxnHash,
|
||||||
|
*txnHash,
|
||||||
|
txnTER,
|
||||||
|
txnHistoryIndex};
|
||||||
|
pushEvent(std::move(e), txnHistoryIndex, l);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PaymentType::user: {
|
||||||
|
if (!txnSuccess)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!deliveredAmt)
|
||||||
|
return;
|
||||||
|
{
|
||||||
|
using namespace event;
|
||||||
|
XChainTransferDetected e{
|
||||||
|
isMainchain_ ? Dir::mainToSide : Dir::sideToMain,
|
||||||
|
*src,
|
||||||
|
*dst,
|
||||||
|
*deliveredAmt,
|
||||||
|
*seq,
|
||||||
|
*txnHash,
|
||||||
|
txnHistoryIndex};
|
||||||
|
pushEvent(std::move(e), txnHistoryIndex, l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// account control tx
|
||||||
|
auto const ledgerIndex = [&]() -> std::optional<std::uint32_t> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return msg["ledger_index"].asInt();
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
JLOGV(j_.error(), "no ledger_index", jv("message", msg));
|
||||||
|
assert(false);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
if (!ledgerIndex)
|
||||||
|
{
|
||||||
|
JLOG(j_.warn()) << "ignoring listener message, no ledgerIndex";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const getSourceTag = [&]() -> std::optional<std::uint32_t> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return msg[jss::transaction]["SourceTag"].asUInt();
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
JLOGV(j_.error(), "wrong SourceTag", jv("message", msg));
|
||||||
|
assert(false);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto const getMemoStr = [&](std::uint32_t index) -> std::string {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (msg[jss::transaction][jss::Memos][index] ==
|
||||||
|
Json::Value::null)
|
||||||
|
return {};
|
||||||
|
auto str = std::string(msg[jss::transaction][jss::Memos][index]
|
||||||
|
[jss::Memo][jss::MemoData]
|
||||||
|
.asString());
|
||||||
|
assert(str.length() <= event::MemoStringMax);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
auto const accountControlType = *accountControlTypeOpt;
|
||||||
|
switch (accountControlType)
|
||||||
|
{
|
||||||
|
case AccountControlType::trigger: {
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"AccountControlType::trigger",
|
||||||
|
jv("chain_name", chainName()),
|
||||||
|
jv("account_seq", *seq),
|
||||||
|
jv("msg", msg));
|
||||||
|
auto sourceTag = getSourceTag();
|
||||||
|
if (!sourceTag)
|
||||||
|
{
|
||||||
|
JLOG(j_.warn())
|
||||||
|
<< "ignoring listener message, no sourceTag";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto memoStr = getMemoStr(0);
|
||||||
|
event::TicketCreateTrigger e = {
|
||||||
|
isMainchain_ ? event::Dir::mainToSide
|
||||||
|
: event::Dir::sideToMain,
|
||||||
|
txnSuccess,
|
||||||
|
0,
|
||||||
|
*ledgerIndex,
|
||||||
|
*txnHash,
|
||||||
|
txnHistoryIndex,
|
||||||
|
*sourceTag,
|
||||||
|
std::move(memoStr)};
|
||||||
|
pushEvent(std::move(e), txnHistoryIndex, l);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AccountControlType::ticket: {
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"AccountControlType::ticket",
|
||||||
|
jv("chain_name", chainName()),
|
||||||
|
jv("account_seq", *seq),
|
||||||
|
jv("msg", msg));
|
||||||
|
auto sourceTag = getSourceTag();
|
||||||
|
if (!sourceTag)
|
||||||
|
{
|
||||||
|
JLOG(j_.warn())
|
||||||
|
<< "ignoring listener message, no sourceTag";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const triggeringTxnHash =
|
||||||
|
detail::getMemoData<uint256>(msg[jss::transaction], 0);
|
||||||
|
if (!triggeringTxnHash)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
(txnSuccess ? j_.trace() : j_.error()),
|
||||||
|
"bootstrap ticket",
|
||||||
|
jv("chain_name", chainName()),
|
||||||
|
jv("account_seq", *seq),
|
||||||
|
jv("msg", msg));
|
||||||
|
|
||||||
|
if (!txnSuccess)
|
||||||
|
return;
|
||||||
|
|
||||||
|
event::BootstrapTicket e = {
|
||||||
|
isMainchain_,
|
||||||
|
txnSuccess,
|
||||||
|
*seq,
|
||||||
|
*ledgerIndex,
|
||||||
|
txnHistoryIndex,
|
||||||
|
*sourceTag};
|
||||||
|
pushEvent(std::move(e), txnHistoryIndex, l);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The TicketCreate tx is both the result of its triggering
|
||||||
|
// AccountSet tx, and the trigger of another account control tx,
|
||||||
|
// if there is a tx in the memo field.
|
||||||
|
event::TicketCreateResult e = {
|
||||||
|
isMainchain_ ? event::Dir::sideToMain
|
||||||
|
: event::Dir::mainToSide,
|
||||||
|
txnSuccess,
|
||||||
|
*seq,
|
||||||
|
*ledgerIndex,
|
||||||
|
*triggeringTxnHash,
|
||||||
|
*txnHash,
|
||||||
|
txnHistoryIndex,
|
||||||
|
*sourceTag,
|
||||||
|
getMemoStr(1)};
|
||||||
|
pushEvent(std::move(e), txnHistoryIndex, l);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AccountControlType::depositAuth: {
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"AccountControlType::depositAuth",
|
||||||
|
jv("chain_name", chainName()),
|
||||||
|
jv("account_seq", *seq),
|
||||||
|
jv("msg", msg));
|
||||||
|
auto const triggeringTxHash =
|
||||||
|
detail::getMemoData<uint256>(msg[jss::transaction], 0);
|
||||||
|
if (!triggeringTxHash)
|
||||||
|
{
|
||||||
|
JLOG(j_.warn())
|
||||||
|
<< "ignoring listener message, no triggeringTxHash";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto opOpt = [&]() -> std::optional<event::AccountFlagOp> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (msg[jss::transaction].isMember(jss::SetFlag) &&
|
||||||
|
msg[jss::transaction][jss::SetFlag].isIntegral())
|
||||||
|
{
|
||||||
|
assert(
|
||||||
|
msg[jss::transaction][jss::SetFlag].asUInt() ==
|
||||||
|
asfDepositAuth);
|
||||||
|
return event::AccountFlagOp::set;
|
||||||
|
}
|
||||||
|
if (msg[jss::transaction].isMember(jss::ClearFlag) &&
|
||||||
|
msg[jss::transaction][jss::ClearFlag].isIntegral())
|
||||||
|
{
|
||||||
|
assert(
|
||||||
|
msg[jss::transaction][jss::ClearFlag]
|
||||||
|
.asUInt() == asfDepositAuth);
|
||||||
|
|
||||||
|
return event::AccountFlagOp::clear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
JLOGV(
|
||||||
|
j_.error(),
|
||||||
|
"unexpected accountSet tx",
|
||||||
|
jv("message", msg));
|
||||||
|
assert(false);
|
||||||
|
return {};
|
||||||
|
}();
|
||||||
|
if (!opOpt)
|
||||||
|
return;
|
||||||
|
|
||||||
|
event::DepositAuthResult e{
|
||||||
|
isMainchain_ ? event::Dir::sideToMain
|
||||||
|
: event::Dir::mainToSide,
|
||||||
|
txnSuccess,
|
||||||
|
*seq,
|
||||||
|
*ledgerIndex,
|
||||||
|
*triggeringTxHash,
|
||||||
|
txnHistoryIndex,
|
||||||
|
*opOpt};
|
||||||
|
pushEvent(std::move(e), txnHistoryIndex, l);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AccountControlType::signerList:
|
||||||
|
// TODO
|
||||||
|
break;
|
||||||
|
case AccountControlType::disableMasterKey: {
|
||||||
|
event::DisableMasterKeyResult e{
|
||||||
|
isMainchain_, *seq, txnHistoryIndex};
|
||||||
|
pushEvent(std::move(e), txnHistoryIndex, l);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Handling "last in history" is done through the lambda given
|
||||||
|
// to `make_scope` earlier in the function
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ChainListener::setLastXChainTxnWithResult(uint256 const& hash)
|
||||||
|
{
|
||||||
|
// Note that `onMessage` also locks this mutex, and it calls
|
||||||
|
// `setLastXChainTxnWithResult`. However, it calls that function on the
|
||||||
|
// other chain, so the mutex will not be locked twice on the same
|
||||||
|
// thread.
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
if (!initialSync_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto const hasReplayed = initialSync_->setLastXChainTxnWithResult(hash);
|
||||||
|
if (hasReplayed)
|
||||||
|
initialSync_.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ChainListener::setNoLastXChainTxnWithResult()
|
||||||
|
{
|
||||||
|
// Note that `onMessage` also locks this mutex, and it calls
|
||||||
|
// `setNoLastXChainTxnWithResult`. However, it calls that function on
|
||||||
|
// the other chain, so the mutex will not be locked twice on the same
|
||||||
|
// thread.
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
if (!initialSync_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool const hasReplayed = initialSync_->setNoLastXChainTxnWithResult();
|
||||||
|
if (hasReplayed)
|
||||||
|
initialSync_.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
ChainListener::getInfo() const
|
||||||
|
{
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
|
||||||
|
Json::Value ret{Json::objectValue};
|
||||||
|
ret[jss::state] = initialSync_ ? "syncing" : "normal";
|
||||||
|
if (initialSync_)
|
||||||
|
{
|
||||||
|
ret[jss::sync_info] = initialSync_->getInfo();
|
||||||
|
}
|
||||||
|
// get the state (in sync, syncing)
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
102
src/ripple/app/sidechain/impl/ChainListener.h
Normal file
102
src/ripple/app/sidechain/impl/ChainListener.h
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#ifndef RIPPLE_SIDECHAIN_IMPL_CHAINLISTENER_H_INCLUDED
|
||||||
|
#define RIPPLE_SIDECHAIN_IMPL_CHAINLISTENER_H_INCLUDED
|
||||||
|
|
||||||
|
#include <ripple/protocol/AccountID.h>
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/impl/InitialSync.h>
|
||||||
|
#include <ripple/beast/utility/Journal.h>
|
||||||
|
#include <boost/asio/io_service.hpp>
|
||||||
|
#include <boost/asio/ip/address.hpp>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
class Federator;
|
||||||
|
class WebsocketClient;
|
||||||
|
|
||||||
|
class ChainListener
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
enum class IsMainchain { no, yes };
|
||||||
|
|
||||||
|
bool const isMainchain_;
|
||||||
|
// Sending xrp to the door account will trigger a x-chain transaction
|
||||||
|
AccountID const doorAccount_;
|
||||||
|
std::string const doorAccountStr_;
|
||||||
|
std::weak_ptr<Federator> federator_;
|
||||||
|
mutable std::mutex m_;
|
||||||
|
// Logic to handle potentially collecting and replaying historical
|
||||||
|
// transactions. Will be empty after replaying.
|
||||||
|
std::unique_ptr<InitialSync> GUARDED_BY(m_) initialSync_;
|
||||||
|
beast::Journal j_;
|
||||||
|
|
||||||
|
ChainListener(
|
||||||
|
IsMainchain isMainchain,
|
||||||
|
AccountID const& account,
|
||||||
|
std::weak_ptr<Federator>&& federator,
|
||||||
|
beast::Journal j);
|
||||||
|
|
||||||
|
virtual ~ChainListener();
|
||||||
|
|
||||||
|
std::string const&
|
||||||
|
chainName() const;
|
||||||
|
|
||||||
|
void
|
||||||
|
processMessage(Json::Value const& msg) EXCLUDES(m_);
|
||||||
|
|
||||||
|
template <class E>
|
||||||
|
void
|
||||||
|
pushEvent(E&& e, int txHistoryIndex, std::lock_guard<std::mutex> const&)
|
||||||
|
REQUIRES(m_);
|
||||||
|
|
||||||
|
public:
|
||||||
|
void
|
||||||
|
setLastXChainTxnWithResult(uint256 const& hash) EXCLUDES(m_);
|
||||||
|
void
|
||||||
|
setNoLastXChainTxnWithResult() EXCLUDES(m_);
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
getInfo() const EXCLUDES(m_);
|
||||||
|
|
||||||
|
using RpcCallback = std::function<void(Json::Value const&)>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* send a RPC and call the callback with the RPC result
|
||||||
|
* @param cmd PRC command
|
||||||
|
* @param params RPC command parameter
|
||||||
|
* @param onResponse callback to process RPC result
|
||||||
|
*/
|
||||||
|
virtual void
|
||||||
|
send(
|
||||||
|
std::string const& cmd,
|
||||||
|
Json::Value const& params,
|
||||||
|
RpcCallback onResponse) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
|
|
||||||
|
#endif
|
||||||
327
src/ripple/app/sidechain/impl/DoorKeeper.cpp
Normal file
327
src/ripple/app/sidechain/impl/DoorKeeper.cpp
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/impl/DoorKeeper.h>
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/Federator.h>
|
||||||
|
#include <ripple/app/sidechain/impl/TicketHolder.h>
|
||||||
|
#include <ripple/basics/Log.h>
|
||||||
|
#include <ripple/json/json_writer.h>
|
||||||
|
#include <ripple/protocol/LedgerFormats.h>
|
||||||
|
#include <ripple/protocol/TxFlags.h>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
DoorKeeper::DoorKeeper(
|
||||||
|
bool isMainChain,
|
||||||
|
AccountID const& account,
|
||||||
|
TicketRunner& ticketRunner,
|
||||||
|
Federator& federator,
|
||||||
|
beast::Journal j)
|
||||||
|
: isMainChain_(isMainChain)
|
||||||
|
, accountStr_(toBase58(account))
|
||||||
|
, ticketRunner_(ticketRunner)
|
||||||
|
, federator_(federator)
|
||||||
|
, j_(j)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DoorKeeper::init()
|
||||||
|
{
|
||||||
|
std::lock_guard lock(mtx_);
|
||||||
|
if (initData_.status_ != InitializeStatus::waitLedger)
|
||||||
|
return;
|
||||||
|
initData_.status_ = InitializeStatus::waitAccountInfo;
|
||||||
|
rpcAccountInfo(lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DoorKeeper::updateQueueLength(std::uint32_t length)
|
||||||
|
{
|
||||||
|
DoorStatus oldStatus;
|
||||||
|
auto const tx = [&]() -> std::optional<Json::Value> {
|
||||||
|
enum Action { setFlag, clearFlag, noAction };
|
||||||
|
Action action = noAction;
|
||||||
|
std::lock_guard lock(mtx_);
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"updateQueueLength",
|
||||||
|
jv("account:", accountStr_),
|
||||||
|
jv("QLen", length),
|
||||||
|
jv("chain", (isMainChain_ ? "main" : "side")));
|
||||||
|
|
||||||
|
if (initData_.status_ != InitializeStatus::initialized)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
oldStatus = status_;
|
||||||
|
if (length >= HighWaterMark && status_ == DoorStatus::open)
|
||||||
|
{
|
||||||
|
action = setFlag;
|
||||||
|
status_ = DoorStatus::closing;
|
||||||
|
}
|
||||||
|
else if (length <= LowWaterMark && status_ == DoorStatus::closed)
|
||||||
|
{
|
||||||
|
action = clearFlag;
|
||||||
|
status_ = DoorStatus::opening;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == noAction)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
XRPAmount const fee{Federator::accountControlTxFee};
|
||||||
|
Json::Value txJson;
|
||||||
|
txJson[jss::TransactionType] = "AccountSet";
|
||||||
|
txJson[jss::Account] = accountStr_;
|
||||||
|
txJson[jss::Sequence] = 0; // to be filled by ticketRunner
|
||||||
|
txJson[jss::Fee] = to_string(fee);
|
||||||
|
if (action == setFlag)
|
||||||
|
txJson[jss::SetFlag] = asfDepositAuth;
|
||||||
|
else
|
||||||
|
txJson[jss::ClearFlag] = asfDepositAuth;
|
||||||
|
return txJson;
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (tx)
|
||||||
|
{
|
||||||
|
bool triggered = false;
|
||||||
|
if (isMainChain_)
|
||||||
|
{
|
||||||
|
triggered =
|
||||||
|
ticketRunner_.trigger(TicketPurpose::mainDoorKeeper, tx, {});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
triggered =
|
||||||
|
ticketRunner_.trigger(TicketPurpose::sideDoorKeeper, {}, tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"updateQueueLength",
|
||||||
|
jv("account:", accountStr_),
|
||||||
|
jv("QLen", length),
|
||||||
|
jv("chain", (isMainChain_ ? "main" : "side")),
|
||||||
|
jv("tx", *tx),
|
||||||
|
jv("triggered", (triggered ? "yes" : "no")));
|
||||||
|
|
||||||
|
if (!triggered)
|
||||||
|
{
|
||||||
|
std::lock_guard lock(mtx_);
|
||||||
|
status_ = oldStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DoorKeeper::onEvent(const event::DepositAuthResult& e)
|
||||||
|
{
|
||||||
|
std::lock_guard lock(mtx_);
|
||||||
|
if (initData_.status_ != InitializeStatus::initialized)
|
||||||
|
{
|
||||||
|
JLOG(j_.trace()) << "Queue an event";
|
||||||
|
initData_.toReplay_.push(e);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
processEvent(e, lock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DoorKeeper::rpcAccountInfo(std::lock_guard<std::mutex> const&)
|
||||||
|
{
|
||||||
|
Json::Value params = [&] {
|
||||||
|
Json::Value r;
|
||||||
|
r[jss::account] = accountStr_;
|
||||||
|
r[jss::ledger_index] = "validated";
|
||||||
|
r[jss::signer_lists] = false;
|
||||||
|
return r;
|
||||||
|
}();
|
||||||
|
|
||||||
|
rpcChannel_->send(
|
||||||
|
"account_info",
|
||||||
|
params,
|
||||||
|
[chain = isMainChain_ ? Federator::mainChain : Federator::sideChain,
|
||||||
|
wp = federator_.weak_from_this()](Json::Value const& response) {
|
||||||
|
if (auto f = wp.lock())
|
||||||
|
f->getDoorKeeper(chain).accountInfoResult(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DoorKeeper::accountInfoResult(const Json::Value& rpcResult)
|
||||||
|
{
|
||||||
|
auto ledgerNFlagsOpt =
|
||||||
|
[&]() -> std::optional<std::pair<std::uint32_t, std::uint32_t>> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (rpcResult.isMember(jss::error))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (!rpcResult[jss::validated].asBool())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (rpcResult[jss::account_data][jss::Account] != accountStr_)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (!rpcResult[jss::account_data][jss::Flags].isIntegral())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (!rpcResult[jss::ledger_index].isIntegral())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_pair(
|
||||||
|
rpcResult[jss::ledger_index].asUInt(),
|
||||||
|
rpcResult[jss::account_data][jss::Flags].asUInt());
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (!ledgerNFlagsOpt)
|
||||||
|
{
|
||||||
|
// should not reach here since we only ask account_object after a
|
||||||
|
// validated ledger
|
||||||
|
JLOGV(j_.error(), "account_info result ", jv("result", rpcResult));
|
||||||
|
assert(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [ledgerIndex, flags] = *ledgerNFlagsOpt;
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"accountInfoResult",
|
||||||
|
jv("ledgerIndex", ledgerIndex),
|
||||||
|
jv("flags", flags));
|
||||||
|
std::lock_guard lock(mtx_);
|
||||||
|
initData_.ledgerIndex_ = ledgerIndex;
|
||||||
|
status_ = (flags & lsfDepositAuth) == 0 ? DoorStatus::open
|
||||||
|
: DoorStatus::closed;
|
||||||
|
while (!initData_.toReplay_.empty())
|
||||||
|
{
|
||||||
|
processEvent(initData_.toReplay_.front(), lock);
|
||||||
|
initData_.toReplay_.pop();
|
||||||
|
}
|
||||||
|
initData_.status_ = InitializeStatus::initialized;
|
||||||
|
JLOG(j_.info()) << "DoorKeeper initialized, status "
|
||||||
|
<< (status_ == DoorStatus::open ? "open" : "closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DoorKeeper::processEvent(
|
||||||
|
const event::DepositAuthResult& e,
|
||||||
|
std::lock_guard<std::mutex> const&)
|
||||||
|
{
|
||||||
|
if (e.ledgerIndex_ <= initData_.ledgerIndex_)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"DepositAuthResult, ignoring an old result",
|
||||||
|
jv("account:", accountStr_),
|
||||||
|
jv("operation",
|
||||||
|
(e.op_ == event::AccountFlagOp::set ? "set" : "clear")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"DepositAuthResult",
|
||||||
|
jv("chain", (isMainChain_ ? "main" : "side")),
|
||||||
|
jv("account:", accountStr_),
|
||||||
|
jv("operation",
|
||||||
|
(e.op_ == event::AccountFlagOp::set ? "set" : "clear")));
|
||||||
|
|
||||||
|
if (!e.success_)
|
||||||
|
{
|
||||||
|
JLOG(j_.error()) << "DepositAuthResult event error, account "
|
||||||
|
<< (isMainChain_ ? "main" : "side") << accountStr_;
|
||||||
|
assert(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (e.op_)
|
||||||
|
{
|
||||||
|
case event::AccountFlagOp::set:
|
||||||
|
assert(
|
||||||
|
status_ == DoorStatus::open || status_ == DoorStatus::closing);
|
||||||
|
status_ = DoorStatus::closed;
|
||||||
|
break;
|
||||||
|
case event::AccountFlagOp::clear:
|
||||||
|
assert(
|
||||||
|
status_ == DoorStatus::closed ||
|
||||||
|
status_ == DoorStatus::opening);
|
||||||
|
status_ = DoorStatus::open;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
DoorKeeper::getInfo() const
|
||||||
|
{
|
||||||
|
auto DoorStatusToStr = [](DoorKeeper::DoorStatus s) -> std::string {
|
||||||
|
switch (s)
|
||||||
|
{
|
||||||
|
case DoorKeeper::DoorStatus::open:
|
||||||
|
return "open";
|
||||||
|
case DoorKeeper::DoorStatus::opening:
|
||||||
|
return "opening";
|
||||||
|
case DoorKeeper::DoorStatus::closed:
|
||||||
|
return "closed";
|
||||||
|
case DoorKeeper::DoorStatus::closing:
|
||||||
|
return "closing";
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
Json::Value ret{Json::objectValue};
|
||||||
|
{
|
||||||
|
std::lock_guard lock{mtx_};
|
||||||
|
if (initData_.status_ == InitializeStatus::initialized)
|
||||||
|
{
|
||||||
|
ret["initialized"] = "true";
|
||||||
|
ret["status"] = DoorStatusToStr(status_);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ret["initialized"] = "false";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DoorKeeper::setRpcChannel(std::shared_ptr<ChainListener> channel)
|
||||||
|
{
|
||||||
|
rpcChannel_ = std::move(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
128
src/ripple/app/sidechain/impl/DoorKeeper.h
Normal file
128
src/ripple/app/sidechain/impl/DoorKeeper.h
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#ifndef RIPPLE_SIDECHAIN_IMPL_DOOR_OPENER_H
|
||||||
|
#define RIPPLE_SIDECHAIN_IMPL_DOOR_OPENER_H
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/FederatorEvents.h>
|
||||||
|
#include <ripple/app/sidechain/impl/ChainListener.h>
|
||||||
|
#include <ripple/beast/utility/Journal.h>
|
||||||
|
#include <ripple/json/json_value.h>
|
||||||
|
#include <ripple/protocol/AccountID.h>
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <queue>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
class TicketRunner;
|
||||||
|
class Federator;
|
||||||
|
|
||||||
|
class DoorKeeper
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static constexpr std::uint32_t LowWaterMark = 0;
|
||||||
|
static constexpr std::uint32_t HighWaterMark = 100;
|
||||||
|
static_assert(HighWaterMark > LowWaterMark);
|
||||||
|
enum class DoorStatus { open, closing, closed, opening };
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class InitializeStatus { waitLedger, waitAccountInfo, initialized };
|
||||||
|
struct InitializeData
|
||||||
|
{
|
||||||
|
InitializeStatus status_ = InitializeStatus::waitLedger;
|
||||||
|
std::queue<event::DepositAuthResult> toReplay_;
|
||||||
|
std::uint32_t ledgerIndex_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::shared_ptr<ChainListener> rpcChannel_;
|
||||||
|
bool const isMainChain_;
|
||||||
|
std::string const accountStr_;
|
||||||
|
mutable std::mutex mtx_;
|
||||||
|
InitializeData GUARDED_BY(mtx_) initData_;
|
||||||
|
DoorStatus GUARDED_BY(mtx_) status_;
|
||||||
|
TicketRunner& ticketRunner_;
|
||||||
|
Federator& federator_;
|
||||||
|
beast::Journal j_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
DoorKeeper(
|
||||||
|
bool isMainChain,
|
||||||
|
AccountID const& account,
|
||||||
|
TicketRunner& ticketRunner,
|
||||||
|
Federator& federator,
|
||||||
|
beast::Journal j);
|
||||||
|
~DoorKeeper() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* start to initialize the doorKeeper by sending accountInfo RPC
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
init() EXCLUDES(mtx_);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* process the accountInfo result and set the door status
|
||||||
|
* This is the end of initialization
|
||||||
|
*
|
||||||
|
* @param rpcResult the accountInfo result
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
accountInfoResult(Json::Value const& rpcResult) EXCLUDES(mtx_);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update the doorKeeper about the number of pending XChain payments
|
||||||
|
* The doorKeeper will close the door if there are too many
|
||||||
|
* pending XChain payments and reopen the door later
|
||||||
|
*
|
||||||
|
* @param length the number of pending XChain payments
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
updateQueueLength(std::uint32_t length) EXCLUDES(mtx_);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* process a DepositAuthResult event and set the door status.
|
||||||
|
* It queues the event if the doorKeeper is not yet initialized.
|
||||||
|
*
|
||||||
|
* @param e the DepositAuthResult event
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
onEvent(event::DepositAuthResult const& e) EXCLUDES(mtx_);
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
getInfo() const EXCLUDES(mtx_);
|
||||||
|
|
||||||
|
void
|
||||||
|
setRpcChannel(std::shared_ptr<ChainListener> channel);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void
|
||||||
|
rpcAccountInfo(std::lock_guard<std::mutex> const&) REQUIRES(mtx_);
|
||||||
|
|
||||||
|
void
|
||||||
|
processEvent(
|
||||||
|
event::DepositAuthResult const& e,
|
||||||
|
std::lock_guard<std::mutex> const&) REQUIRES(mtx_);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
|
|
||||||
|
#endif
|
||||||
586
src/ripple/app/sidechain/impl/InitialSync.cpp
Normal file
586
src/ripple/app/sidechain/impl/InitialSync.cpp
Normal file
@@ -0,0 +1,586 @@
|
|||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/impl/InitialSync.h>
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/Federator.h>
|
||||||
|
#include <ripple/basics/Log.h>
|
||||||
|
#include <ripple/basics/contract.h>
|
||||||
|
#include <ripple/json/json_writer.h>
|
||||||
|
|
||||||
|
#include <type_traits>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
InitialSync::InitialSync(
|
||||||
|
std::weak_ptr<Federator> federator,
|
||||||
|
bool isMainchain,
|
||||||
|
beast::Journal j)
|
||||||
|
: federator_{std::move(federator)}, isMainchain_{isMainchain}, j_{j}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
InitialSync::hasTransaction(
|
||||||
|
uint256 const& txnHash,
|
||||||
|
std::lock_guard<std::mutex> const&) const
|
||||||
|
{
|
||||||
|
return seenTriggeringTxns_.count(txnHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
InitialSync::canReplay(std::lock_guard<std::mutex> const&) const
|
||||||
|
{
|
||||||
|
return !(
|
||||||
|
needsLastXChainTxn_ || needsOtherChainLastXChainTxn_ ||
|
||||||
|
needsReplayStartTxnHash_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
InitialSync::stopHistoricalTxns(std::lock_guard<std::mutex> const&)
|
||||||
|
{
|
||||||
|
if (!acquiringHistoricData_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
acquiringHistoricData_ = false;
|
||||||
|
if (auto f = federator_.lock())
|
||||||
|
{
|
||||||
|
f->stopHistoricalTxns(getChainType(isMainchain_));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
InitialSync::done()
|
||||||
|
{
|
||||||
|
if (auto f = federator_.lock())
|
||||||
|
{
|
||||||
|
f->initialSyncDone(
|
||||||
|
isMainchain_ ? Federator::ChainType::mainChain
|
||||||
|
: Federator::ChainType::sideChain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
InitialSync::setLastXChainTxnWithResult(uint256 const& hash)
|
||||||
|
{
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"last xchain txn with result",
|
||||||
|
jv("needsOtherChainLastXChainTxn", needsOtherChainLastXChainTxn_),
|
||||||
|
jv("isMainchain", isMainchain_),
|
||||||
|
jv("hash", hash));
|
||||||
|
assert(lastXChainTxnWithResult_.value_or(hash) == hash);
|
||||||
|
if (hasReplayed_ || lastXChainTxnWithResult_)
|
||||||
|
return hasReplayed_;
|
||||||
|
|
||||||
|
lastXChainTxnWithResult_ = hash;
|
||||||
|
needsReplayStartTxnHash_ = false;
|
||||||
|
if (needsLastXChainTxn_)
|
||||||
|
{
|
||||||
|
needsLastXChainTxn_ =
|
||||||
|
!seenTriggeringTxns_.count(*lastXChainTxnWithResult_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acquiringHistoricData_ && needsLastXChainTxn_)
|
||||||
|
LogicError("Initial sync could not find historic XChain transaction");
|
||||||
|
|
||||||
|
if (canReplay(l))
|
||||||
|
replay(l);
|
||||||
|
|
||||||
|
return hasReplayed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
InitialSync::setNoLastXChainTxnWithResult()
|
||||||
|
{
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"no last xchain txn with result",
|
||||||
|
jv("needsOtherChainLastXChainTxn", needsOtherChainLastXChainTxn_),
|
||||||
|
jv("isMainchain", isMainchain_));
|
||||||
|
assert(!lastXChainTxnWithResult_);
|
||||||
|
if (hasReplayed_)
|
||||||
|
return hasReplayed_;
|
||||||
|
|
||||||
|
needsLastXChainTxn_ = false;
|
||||||
|
needsReplayStartTxnHash_ = false;
|
||||||
|
|
||||||
|
if (canReplay(l))
|
||||||
|
replay(l);
|
||||||
|
|
||||||
|
return hasReplayed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
InitialSync::replay(std::lock_guard<std::mutex> const& l)
|
||||||
|
{
|
||||||
|
if (hasReplayed_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert(canReplay(l));
|
||||||
|
|
||||||
|
// Note that this function may push a large number of events to the
|
||||||
|
// federator, and it runs under a lock. However, pushing an event to the
|
||||||
|
// federator just copies it into a collection (it does not handle the event
|
||||||
|
// in the same thread). So this should run relatively quickly.
|
||||||
|
stopHistoricalTxns(l);
|
||||||
|
hasReplayed_ = true;
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"InitialSync replay,",
|
||||||
|
jv("chain_name", (isMainchain_ ? "Mainchain" : "Sidechain")),
|
||||||
|
jv("lastXChainTxnWithResult_",
|
||||||
|
(lastXChainTxnWithResult_ ? strHex(*lastXChainTxnWithResult_)
|
||||||
|
: "not set")));
|
||||||
|
|
||||||
|
if (lastXChainTxnWithResult_)
|
||||||
|
assert(seenTriggeringTxns_.count(*lastXChainTxnWithResult_));
|
||||||
|
|
||||||
|
if (lastXChainTxnWithResult_ &&
|
||||||
|
seenTriggeringTxns_.count(*lastXChainTxnWithResult_))
|
||||||
|
{
|
||||||
|
// Remove the XChainTransferDetected event associated with this txn, and
|
||||||
|
// all the XChainTransferDetected events before it. They have already
|
||||||
|
// been submitted. If they are not removed, they will never collect
|
||||||
|
// enough signatures to be submitted (since the other federators have
|
||||||
|
// already submitted it), and it will prevent subsequent event from
|
||||||
|
// replaying.
|
||||||
|
std::vector<decltype(pendingEvents_)::const_iterator> toRemove;
|
||||||
|
toRemove.reserve(pendingEvents_.size());
|
||||||
|
std::vector<decltype(pendingEvents_)::const_iterator> toRemoveTrigger;
|
||||||
|
bool matched = false;
|
||||||
|
for (auto i = pendingEvents_.cbegin(), e = pendingEvents_.cend();
|
||||||
|
i != e;
|
||||||
|
++i)
|
||||||
|
{
|
||||||
|
auto const et = eventType(i->second);
|
||||||
|
if (et == event::EventType::trigger)
|
||||||
|
{
|
||||||
|
toRemove.push_back(i);
|
||||||
|
}
|
||||||
|
else if (et == event::EventType::resultAndTrigger)
|
||||||
|
{
|
||||||
|
toRemoveTrigger.push_back(i);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const txnHash = sidechain::txnHash(i->second);
|
||||||
|
if (!txnHash)
|
||||||
|
{
|
||||||
|
// All triggering events should have a txnHash
|
||||||
|
assert(0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"InitialSync replay, remove trigger event from pendingEvents_",
|
||||||
|
jv("chain_name", (isMainchain_ ? "Mainchain" : "Sidechain")),
|
||||||
|
jv("txn", toJson(i->second)));
|
||||||
|
if (*lastXChainTxnWithResult_ == *txnHash)
|
||||||
|
{
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(matched);
|
||||||
|
if (matched)
|
||||||
|
{
|
||||||
|
for (auto const& i : toRemoveTrigger)
|
||||||
|
{
|
||||||
|
if (auto ticketResult = std::get_if<event::TicketCreateResult>(
|
||||||
|
&(pendingEvents_.erase(i, i)->second));
|
||||||
|
ticketResult)
|
||||||
|
{
|
||||||
|
ticketResult->removeTrigger();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto const& i : toRemove)
|
||||||
|
{
|
||||||
|
pendingEvents_.erase(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disableMasterKeySeq_)
|
||||||
|
{
|
||||||
|
// Remove trigger events that come beofre disableMasterKeySeq_
|
||||||
|
std::vector<decltype(pendingEvents_)::const_iterator> toRemove;
|
||||||
|
toRemove.reserve(pendingEvents_.size());
|
||||||
|
for (auto i = pendingEvents_.cbegin(), e = pendingEvents_.cend();
|
||||||
|
i != e;
|
||||||
|
++i)
|
||||||
|
{
|
||||||
|
if (std::holds_alternative<event::DisableMasterKeyResult>(
|
||||||
|
i->second))
|
||||||
|
break;
|
||||||
|
if (eventType(i->second) != event::EventType::trigger)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
toRemove.push_back(i);
|
||||||
|
}
|
||||||
|
for (auto const& i : toRemove)
|
||||||
|
{
|
||||||
|
pendingEvents_.erase(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto f = federator_.lock())
|
||||||
|
{
|
||||||
|
for (auto&& [_, e] : pendingEvents_)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"InitialSync replay, pushing event",
|
||||||
|
jv("chain_name", (isMainchain_ ? "Mainchain" : "Sidechain")),
|
||||||
|
jv("txn", toJson(e)));
|
||||||
|
f->push(std::move(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seenTriggeringTxns_.clear();
|
||||||
|
pendingEvents_.clear();
|
||||||
|
if (auto f = federator_.lock())
|
||||||
|
{
|
||||||
|
auto const key = isMainchain_ ? Federator::UnlockMainLoopKey::mainChain
|
||||||
|
: Federator::UnlockMainLoopKey::sideChain;
|
||||||
|
f->unlockMainLoop(key);
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
InitialSync::onEvent(event::XChainTransferDetected&& e)
|
||||||
|
{
|
||||||
|
return onTriggerEvent(std::move(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
InitialSync::onEvent(event::XChainTransferResult&& e)
|
||||||
|
{
|
||||||
|
return onResultEvent(std::move(e), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
InitialSync::onEvent(event::TicketCreateTrigger&& e)
|
||||||
|
{
|
||||||
|
return onTriggerEvent(std::move(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
InitialSync::onEvent(event::TicketCreateResult&& e)
|
||||||
|
{
|
||||||
|
static_assert(std::is_rvalue_reference_v<decltype(e)>, "");
|
||||||
|
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
if (hasReplayed_)
|
||||||
|
{
|
||||||
|
assert(0);
|
||||||
|
return hasReplayed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(), "InitialSync TicketCreateResult", jv("event", e.toJson()));
|
||||||
|
|
||||||
|
if (needsOtherChainLastXChainTxn_)
|
||||||
|
{
|
||||||
|
if (auto f = federator_.lock())
|
||||||
|
{
|
||||||
|
// Inform the other sync object that the last transaction with a
|
||||||
|
// result was found. e.dir_ is for the triggering transaction.
|
||||||
|
Federator::ChainType const chainType = srcChainType(e.dir_);
|
||||||
|
f->setLastXChainTxnWithResult(
|
||||||
|
chainType, e.txnSeq_, 2, e.srcChainTxnHash_);
|
||||||
|
}
|
||||||
|
needsOtherChainLastXChainTxn_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!e.memoStr_.empty())
|
||||||
|
{
|
||||||
|
seenTriggeringTxns_.insert(e.txnHash_);
|
||||||
|
if (lastXChainTxnWithResult_ && needsLastXChainTxn_)
|
||||||
|
{
|
||||||
|
if (e.txnHash_ == *lastXChainTxnWithResult_)
|
||||||
|
{
|
||||||
|
needsLastXChainTxn_ = false;
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"InitialSync TicketCreateResult, found the trigger tx",
|
||||||
|
jv("txHash", e.txnHash_),
|
||||||
|
jv("chain_name",
|
||||||
|
(isMainchain_ ? "Mainchain" : "Sidechain")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingEvents_[e.rpcOrder_] = std::move(e);
|
||||||
|
|
||||||
|
if (canReplay(l))
|
||||||
|
replay(l);
|
||||||
|
|
||||||
|
return hasReplayed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
InitialSync::onEvent(event::DepositAuthResult&& e)
|
||||||
|
{
|
||||||
|
return onResultEvent(std::move(e), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
InitialSync::onEvent(event::BootstrapTicket&& e)
|
||||||
|
{
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
|
||||||
|
JLOGV(j_.trace(), "InitialSync onBootstrapTicket", jv("event", e.toJson()));
|
||||||
|
|
||||||
|
if (hasReplayed_)
|
||||||
|
{
|
||||||
|
assert(0);
|
||||||
|
return hasReplayed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingEvents_[e.rpcOrder_] = std::move(e);
|
||||||
|
|
||||||
|
if (canReplay(l))
|
||||||
|
replay(l);
|
||||||
|
|
||||||
|
return hasReplayed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
InitialSync::onEvent(event::DisableMasterKeyResult&& e)
|
||||||
|
{
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"InitialSync onDisableMasterKeyResultEvent",
|
||||||
|
jv("event", e.toJson()));
|
||||||
|
assert(!disableMasterKeySeq_);
|
||||||
|
disableMasterKeySeq_ = e.txnSeq_;
|
||||||
|
|
||||||
|
if (lastXChainTxnWithResult_)
|
||||||
|
LogicError("Initial sync could not find historic XChain transaction");
|
||||||
|
|
||||||
|
if (needsOtherChainLastXChainTxn_)
|
||||||
|
{
|
||||||
|
if (auto f = federator_.lock())
|
||||||
|
{
|
||||||
|
// Inform the other sync object that the last transaction
|
||||||
|
// with a result was found. Note that if start of historic
|
||||||
|
// transactions is found while listening to the mainchain, the
|
||||||
|
// _sidechain_ listener needs to be informed that there is no last
|
||||||
|
// cross chain transaction with result.
|
||||||
|
Federator::ChainType const chainType = getChainType(!isMainchain_);
|
||||||
|
f->setNoLastXChainTxnWithResult(chainType);
|
||||||
|
}
|
||||||
|
needsOtherChainLastXChainTxn_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasReplayed_)
|
||||||
|
{
|
||||||
|
assert(0);
|
||||||
|
return hasReplayed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto f = federator_.lock())
|
||||||
|
{
|
||||||
|
// Set the account sequence right away. Otherwise when replaying, a
|
||||||
|
// triggering transaction from the main chain can be replayed before the
|
||||||
|
// disable master key event on the side chain, and the sequence number
|
||||||
|
// will be wrong.
|
||||||
|
f->setAccountSeqMax(getChainType(e.isMainchain_), e.txnSeq_ + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingEvents_[e.rpcOrder_] = std::move(e);
|
||||||
|
|
||||||
|
if (canReplay(l))
|
||||||
|
replay(l);
|
||||||
|
|
||||||
|
return hasReplayed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
bool
|
||||||
|
InitialSync::onTriggerEvent(T&& e)
|
||||||
|
{
|
||||||
|
static_assert(std::is_rvalue_reference_v<decltype(e)>, "");
|
||||||
|
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
if (hasReplayed_)
|
||||||
|
{
|
||||||
|
assert(0);
|
||||||
|
return hasReplayed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
JLOGV(j_.trace(), "InitialSync onTriggerEvent", jv("event", e.toJson()));
|
||||||
|
seenTriggeringTxns_.insert(e.txnHash_);
|
||||||
|
if (lastXChainTxnWithResult_ && needsLastXChainTxn_)
|
||||||
|
{
|
||||||
|
if (e.txnHash_ == *lastXChainTxnWithResult_)
|
||||||
|
{
|
||||||
|
needsLastXChainTxn_ = false;
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"InitialSync onTriggerEvent, found the trigger tx",
|
||||||
|
jv("txHash", e.txnHash_),
|
||||||
|
jv("chain_name", (isMainchain_ ? "Mainchain" : "Sidechain")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingEvents_[e.rpcOrder_] = std::move(e);
|
||||||
|
|
||||||
|
if (canReplay(l))
|
||||||
|
{
|
||||||
|
replay(l);
|
||||||
|
}
|
||||||
|
return hasReplayed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
bool
|
||||||
|
InitialSync::onResultEvent(T&& e, std::uint32_t seqTook)
|
||||||
|
{
|
||||||
|
static_assert(std::is_rvalue_reference_v<decltype(e)>, "");
|
||||||
|
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
if (hasReplayed_)
|
||||||
|
{
|
||||||
|
assert(0);
|
||||||
|
return hasReplayed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
JLOGV(j_.trace(), "InitialSync onResultEvent", jv("event", e.toJson()));
|
||||||
|
|
||||||
|
if (needsOtherChainLastXChainTxn_)
|
||||||
|
{
|
||||||
|
if (auto f = federator_.lock())
|
||||||
|
{
|
||||||
|
// Inform the other sync object that the last transaction with a
|
||||||
|
// result was found. e.dir_ is for the triggering transaction.
|
||||||
|
Federator::ChainType const chainType = srcChainType(e.dir_);
|
||||||
|
f->setLastXChainTxnWithResult(
|
||||||
|
chainType, e.txnSeq_, seqTook, e.srcChainTxnHash_);
|
||||||
|
}
|
||||||
|
needsOtherChainLastXChainTxn_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingEvents_[e.rpcOrder_] = std::move(e);
|
||||||
|
|
||||||
|
if (canReplay(l))
|
||||||
|
replay(l);
|
||||||
|
|
||||||
|
return hasReplayed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
InitialSync::onEvent(event::RefundTransferResult&& e)
|
||||||
|
{
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
if (hasReplayed_)
|
||||||
|
{
|
||||||
|
assert(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pendingEvents_[e.rpcOrder_] = std::move(e);
|
||||||
|
|
||||||
|
if (canReplay(l))
|
||||||
|
replay(l);
|
||||||
|
}
|
||||||
|
return hasReplayed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
getInfo(FederatorEvent const& event)
|
||||||
|
{
|
||||||
|
return std::visit(
|
||||||
|
[](auto const& e) {
|
||||||
|
using eventType = decltype(e);
|
||||||
|
Json::Value ret{Json::objectValue};
|
||||||
|
if constexpr (std::is_same_v<
|
||||||
|
eventType,
|
||||||
|
event::XChainTransferDetected>)
|
||||||
|
{
|
||||||
|
ret[jss::type] = "xchain_transfer_detected";
|
||||||
|
ret[jss::amount] = to_string(e.amt_);
|
||||||
|
ret[jss::destination_account] = to_string(e.dst_);
|
||||||
|
ret[jss::hash] = strHex(e.txnHash_);
|
||||||
|
ret[jss::sequence] = e.txnSeq_;
|
||||||
|
ret["rpc_order"] = e.rpcOrder_;
|
||||||
|
}
|
||||||
|
else if constexpr (std::is_same_v<
|
||||||
|
eventType,
|
||||||
|
event::XChainTransferResult>)
|
||||||
|
{
|
||||||
|
ret[jss::type] = "xchain_transfer_result";
|
||||||
|
ret[jss::amount] = to_string(e.amt_);
|
||||||
|
ret[jss::destination_account] = to_string(e.dst_);
|
||||||
|
ret[jss::hash] = strHex(e.txnHash_);
|
||||||
|
ret["triggering_tx_hash"] = strHex(e.triggeringTxnHash_);
|
||||||
|
ret[jss::sequence] = e.txnSeq_;
|
||||||
|
ret[jss::result] = transHuman(e.ter_);
|
||||||
|
ret["rpc_order"] = e.rpcOrder_;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ret[jss::type] = "other_event";
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
event);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
InitialSync::getInfo() const
|
||||||
|
{
|
||||||
|
Json::Value ret{Json::objectValue};
|
||||||
|
{
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
ret["last_x_chain_txn_with_result"] = lastXChainTxnWithResult_
|
||||||
|
? strHex(*lastXChainTxnWithResult_)
|
||||||
|
: "None";
|
||||||
|
Json::Value triggerinTxns{Json::arrayValue};
|
||||||
|
for (auto const& h : seenTriggeringTxns_)
|
||||||
|
{
|
||||||
|
triggerinTxns.append(strHex(h));
|
||||||
|
}
|
||||||
|
ret["seen_triggering_txns"] = triggerinTxns;
|
||||||
|
ret["needs_last_x_chain_txn"] = needsLastXChainTxn_;
|
||||||
|
ret["needs_other_chain_last_x_chain_txn"] =
|
||||||
|
needsOtherChainLastXChainTxn_;
|
||||||
|
ret["acquiring_historic_data"] = acquiringHistoricData_;
|
||||||
|
ret["needs_replay_start_txn_hash"] = needsReplayStartTxnHash_;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
|
||||||
|
} // namespace ripple
|
||||||
212
src/ripple/app/sidechain/impl/InitialSync.h
Normal file
212
src/ripple/app/sidechain/impl/InitialSync.h
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#ifndef RIPPLE_SIDECHAIN_IMPL_INITIALSYNC_H_INCLUDED
|
||||||
|
#define RIPPLE_SIDECHAIN_IMPL_INITIALSYNC_H_INCLUDED
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/FederatorEvents.h>
|
||||||
|
#include <ripple/basics/ThreadSaftyAnalysis.h>
|
||||||
|
#include <ripple/basics/UnorderedContainers.h>
|
||||||
|
#include <ripple/beast/utility/Journal.h>
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
class Federator;
|
||||||
|
class WebsocketClient;
|
||||||
|
|
||||||
|
// This class handles the logic of getting a federator that joins the network
|
||||||
|
// into a "normal" state of handling new cross chain transactions and results.
|
||||||
|
// There will be two instance of this class, one for the main chain and one for
|
||||||
|
// the side chain.
|
||||||
|
//
|
||||||
|
// When a federator joins the network of other federators, the network can be in
|
||||||
|
// one of three states:
|
||||||
|
//
|
||||||
|
// 1) The initial sidechain startup.
|
||||||
|
// 2) Running normally with a quorum of federators. This federator that's
|
||||||
|
// joining just increases the quorum.
|
||||||
|
// 3) A stalled sidechain without enough federators to make forward progress.
|
||||||
|
// This federator may or may not increase the quorum enough so cross chain
|
||||||
|
// transactions can continue. In the meantime, cross chain transactions may
|
||||||
|
// continue to accumulate.
|
||||||
|
//
|
||||||
|
// No matter the state of the federators network, connecting to the network goes
|
||||||
|
// through the same steps. There are two instances of this class, one
|
||||||
|
// for the main chain and one for the side chain.
|
||||||
|
//
|
||||||
|
// The RPC command used to fetch transactions will initially be configured to
|
||||||
|
// retrieve both historical transactions and new transactions. Once the
|
||||||
|
// information needed from the historical transactions are retrieved, it will be
|
||||||
|
// changed to only stream new transactions.
|
||||||
|
//
|
||||||
|
// There are two states this class can be in: pre-replay and post-replay. In
|
||||||
|
// pre-replay mode, the class collects information from both historic and
|
||||||
|
// transactions that will be used for helping the this instance and the "other"
|
||||||
|
// instance of this class known when to stop collecting historic data, as well
|
||||||
|
// as collecting transactions for replaying.
|
||||||
|
//
|
||||||
|
// Historic data needs to be collected until:
|
||||||
|
//
|
||||||
|
// 1) The most recent historic `XChainTransferResult` event is detected (or the
|
||||||
|
// account's first transaction is detected). This is used to inform the "other"
|
||||||
|
// instance of this class which `XChainTransferDetected` event is the first that
|
||||||
|
// may need to be replayed. Since the previous `XChainTransferDetected` events
|
||||||
|
// have results on the other chain, we can definitively say the federators have
|
||||||
|
// handled these events and they don't need to be replayed.
|
||||||
|
//
|
||||||
|
// 2) Once the `lastXChainTxnWithResult_` is know, historic transactions need be
|
||||||
|
// acquired until that transaction is seen on a `XChainTransferDetected` event.
|
||||||
|
//
|
||||||
|
// Once historic data collection has completed, the collected transactions are
|
||||||
|
// replayed to the federator, and this class is not longer needed. All new
|
||||||
|
// transactions should simply be forwarded to the federator.
|
||||||
|
//
|
||||||
|
class InitialSync
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::weak_ptr<Federator> federator_;
|
||||||
|
// Holds all the eventsseen so far. These events will be replayed to the
|
||||||
|
// federator upon switching to `normal` mode. Will be cleared while
|
||||||
|
// replaying.
|
||||||
|
std::map<std::int32_t, FederatorEvent> GUARDED_BY(m_) pendingEvents_;
|
||||||
|
// Holds all triggering cross chain transactions seen so far. This is used
|
||||||
|
// to determine if the `XChainTransferDetected` event with the
|
||||||
|
// `lastXChainTxnWithResult_` has has been seen or not. Will be cleared
|
||||||
|
// while replaying
|
||||||
|
hash_set<uint256> GUARDED_BY(m_) seenTriggeringTxns_;
|
||||||
|
// Hash of the last cross chain transaction on this chain with a result on
|
||||||
|
// the "other" chain. Note: this is set when the `InitialSync` for the
|
||||||
|
// "other" chain encounters the transaction.
|
||||||
|
std::optional<uint256> GUARDED_BY(m_) lastXChainTxnWithResult_;
|
||||||
|
// Track if we need to keep acquiring historic transactions for the
|
||||||
|
// `lastXChainTxnWithResult_`. This is true if the lastXChainTxnWithResult_
|
||||||
|
// is unknown, or it is known and the transaction is not part of that
|
||||||
|
// collection yet.
|
||||||
|
bool GUARDED_BY(m_) needsLastXChainTxn_{true};
|
||||||
|
// Track if we need to keep acquiring historic transactions for the other
|
||||||
|
// chain's `lastXChainTxnWithResult_` hash value. This is true if no
|
||||||
|
// cross chain transaction results are known and the first historical
|
||||||
|
// transaction has not been encountered.
|
||||||
|
bool GUARDED_BY(m_) needsOtherChainLastXChainTxn_{true};
|
||||||
|
// Track if the transaction to start the replay from is known. This is true
|
||||||
|
// until `lastXChainTxnWithResult_` is known and the other listener has not
|
||||||
|
// encountered the first historical transaction
|
||||||
|
bool GUARDED_BY(m_) needsReplayStartTxnHash_{true};
|
||||||
|
// True if the historical transactions have been replayed to the federator
|
||||||
|
bool GUARDED_BY(m_) hasReplayed_{false};
|
||||||
|
// Track the state of the transaction data we are acquiring.
|
||||||
|
// If this is `false`, only new transactions events will be streamed.
|
||||||
|
// Note: there will be a period where this is `false` but historic txns will
|
||||||
|
// continue to come in until the rpc command has responded to the request to
|
||||||
|
// shut off historic data.
|
||||||
|
bool GUARDED_BY(m_) acquiringHistoricData_{true};
|
||||||
|
// All transactions before "DisableMasterKey" are setup transactions and
|
||||||
|
// should be ignored
|
||||||
|
std::optional<std::uint32_t> GUARDED_BY(m_) disableMasterKeySeq_;
|
||||||
|
bool const isMainchain_;
|
||||||
|
mutable std::mutex m_;
|
||||||
|
beast::Journal j_;
|
||||||
|
// See description on class for explanation of states
|
||||||
|
|
||||||
|
public:
|
||||||
|
InitialSync(
|
||||||
|
std::weak_ptr<Federator> federator,
|
||||||
|
bool isMainchain,
|
||||||
|
beast::Journal j);
|
||||||
|
|
||||||
|
// Return `hasReplayed_`. This is used to determine if events should
|
||||||
|
// continue to be routed to this object. Once replayed, events can be
|
||||||
|
// processed normally.
|
||||||
|
[[nodiscard]] bool
|
||||||
|
setLastXChainTxnWithResult(uint256 const& hash) EXCLUDES(m_);
|
||||||
|
|
||||||
|
// There have not been any cross chain transactions.
|
||||||
|
// Return `hasReplayed_`. This is used to determine if events should
|
||||||
|
// continue to be routed to this object. Once replayed, events can be
|
||||||
|
// processed normally.
|
||||||
|
[[nodiscard]] bool
|
||||||
|
setNoLastXChainTxnWithResult() EXCLUDES(m_);
|
||||||
|
|
||||||
|
// Return `hasReplayed_`. This is used to determine if events should
|
||||||
|
// continue to be routed to this object. Once replayed, events can be
|
||||||
|
// processed normally.
|
||||||
|
[[nodiscard]] bool
|
||||||
|
onEvent(event::XChainTransferDetected&& e) EXCLUDES(m_);
|
||||||
|
|
||||||
|
// Return `hasReplayed_`. This is used to determine if events should
|
||||||
|
// continue to be routed to this object. Once replayed, events can be
|
||||||
|
// processed normally.
|
||||||
|
[[nodiscard]] bool
|
||||||
|
onEvent(event::XChainTransferResult&& e) EXCLUDES(m_);
|
||||||
|
|
||||||
|
// Return `hasReplayed_`. This is used to determine if events should
|
||||||
|
// continue to be routed to this object. Once replayed, events can be
|
||||||
|
// processed normally.
|
||||||
|
[[nodiscard]] bool
|
||||||
|
onEvent(event::RefundTransferResult&& e) EXCLUDES(m_);
|
||||||
|
|
||||||
|
// Return `hasReplayed_`. This is used to determine if events should
|
||||||
|
// continue to be routed to this object. Once replayed, events can be
|
||||||
|
// processed normally.
|
||||||
|
// Return `hasReplayed_`.
|
||||||
|
[[nodiscard]] bool
|
||||||
|
onEvent(event::TicketCreateTrigger&& e) EXCLUDES(m_);
|
||||||
|
// Return `hasReplayed_`.
|
||||||
|
[[nodiscard]] bool
|
||||||
|
onEvent(event::TicketCreateResult&& e) EXCLUDES(m_);
|
||||||
|
// Return `hasReplayed_`.
|
||||||
|
[[nodiscard]] bool
|
||||||
|
onEvent(event::DepositAuthResult&& e) EXCLUDES(m_);
|
||||||
|
[[nodiscard]] bool
|
||||||
|
onEvent(event::BootstrapTicket&& e) EXCLUDES(m_);
|
||||||
|
[[nodiscard]] bool
|
||||||
|
onEvent(event::DisableMasterKeyResult&& e) EXCLUDES(m_);
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
getInfo() const EXCLUDES(m_);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Replay when historical transactions are no longer being acquired,
|
||||||
|
// and the transaction to start the replay from is known.
|
||||||
|
bool
|
||||||
|
canReplay(std::lock_guard<std::mutex> const&) const REQUIRES(m_);
|
||||||
|
void
|
||||||
|
replay(std::lock_guard<std::mutex> const&) REQUIRES(m_);
|
||||||
|
bool
|
||||||
|
hasTransaction(uint256 const& txnHash, std::lock_guard<std::mutex> const&)
|
||||||
|
const REQUIRES(m_);
|
||||||
|
void
|
||||||
|
stopHistoricalTxns(std::lock_guard<std::mutex> const&) REQUIRES(m_);
|
||||||
|
template <class T>
|
||||||
|
bool
|
||||||
|
onTriggerEvent(T&& e);
|
||||||
|
template <class T>
|
||||||
|
bool
|
||||||
|
onResultEvent(T&& e, std::uint32_t seqTook);
|
||||||
|
void
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
|
|
||||||
|
#endif
|
||||||
147
src/ripple/app/sidechain/impl/MainchainListener.cpp
Normal file
147
src/ripple/app/sidechain/impl/MainchainListener.cpp
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/impl/MainchainListener.h>
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/Federator.h>
|
||||||
|
#include <ripple/app/sidechain/FederatorEvents.h>
|
||||||
|
#include <ripple/app/sidechain/impl/WebsocketClient.h>
|
||||||
|
#include <ripple/basics/Log.h>
|
||||||
|
#include <ripple/basics/XRPAmount.h>
|
||||||
|
#include <ripple/json/Output.h>
|
||||||
|
#include <ripple/json/json_writer.h>
|
||||||
|
#include <ripple/protocol/AccountID.h>
|
||||||
|
#include <ripple/protocol/STAmount.h>
|
||||||
|
#include <ripple/protocol/jss.h>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
class Federator;
|
||||||
|
|
||||||
|
MainchainListener::MainchainListener(
|
||||||
|
AccountID const& account,
|
||||||
|
std::weak_ptr<Federator>&& federator,
|
||||||
|
beast::Journal j)
|
||||||
|
: ChainListener(
|
||||||
|
ChainListener::IsMainchain::yes,
|
||||||
|
account,
|
||||||
|
std::move(federator),
|
||||||
|
j)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
void
|
||||||
|
MainchainListener::onMessage(Json::Value const& msg)
|
||||||
|
{
|
||||||
|
auto callbackOpt = [&]() -> std::optional<RpcCallback> {
|
||||||
|
if (msg.isMember(jss::id) && msg[jss::id].isIntegral())
|
||||||
|
{
|
||||||
|
auto callbackId = msg[jss::id].asUInt();
|
||||||
|
std::lock_guard lock(callbacksMtx_);
|
||||||
|
auto i = callbacks_.find(callbackId);
|
||||||
|
if (i != callbacks_.end())
|
||||||
|
{
|
||||||
|
auto cb = i->second;
|
||||||
|
callbacks_.erase(i);
|
||||||
|
return cb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (callbackOpt)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"Mainchain onMessage, reply to a callback",
|
||||||
|
jv("msg", msg));
|
||||||
|
assert(msg.isMember(jss::result));
|
||||||
|
(*callbackOpt)(msg[jss::result]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
processMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MainchainListener::init(
|
||||||
|
boost::asio::io_service& ios,
|
||||||
|
boost::asio::ip::address const& ip,
|
||||||
|
std::uint16_t port)
|
||||||
|
{
|
||||||
|
wsClient_ = std::make_unique<WebsocketClient>(
|
||||||
|
[self = shared_from_this()](Json::Value const& msg) {
|
||||||
|
self->onMessage(msg);
|
||||||
|
},
|
||||||
|
ios,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
/*headers*/ std::unordered_map<std::string, std::string>{},
|
||||||
|
j_);
|
||||||
|
|
||||||
|
Json::Value params;
|
||||||
|
params[jss::account_history_tx_stream] = Json::objectValue;
|
||||||
|
params[jss::account_history_tx_stream][jss::account] = doorAccountStr_;
|
||||||
|
send("subscribe", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// destructor must be defined after WebsocketClient size is known (i.e. it can
|
||||||
|
// not be defaulted in the header or the unique_ptr declration of
|
||||||
|
// WebsocketClient won't work)
|
||||||
|
MainchainListener::~MainchainListener() = default;
|
||||||
|
|
||||||
|
void
|
||||||
|
MainchainListener::shutdown()
|
||||||
|
{
|
||||||
|
if (wsClient_)
|
||||||
|
wsClient_->shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t
|
||||||
|
MainchainListener::send(std::string const& cmd, Json::Value const& params)
|
||||||
|
{
|
||||||
|
return wsClient_->send(cmd, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MainchainListener::stopHistoricalTxns()
|
||||||
|
{
|
||||||
|
Json::Value params;
|
||||||
|
params[jss::account_history_tx_stream] = Json::objectValue;
|
||||||
|
params[jss::account_history_tx_stream][jss::stop_history_tx_only] = true;
|
||||||
|
params[jss::account_history_tx_stream][jss::account] = doorAccountStr_;
|
||||||
|
send("unsubscribe", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MainchainListener::send(
|
||||||
|
std::string const& cmd,
|
||||||
|
Json::Value const& params,
|
||||||
|
RpcCallback onResponse)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(), "Mainchain send", jv("command", cmd), jv("params", params));
|
||||||
|
|
||||||
|
auto id = wsClient_->send(cmd, params);
|
||||||
|
std::lock_guard lock(callbacksMtx_);
|
||||||
|
callbacks_.emplace(id, onResponse);
|
||||||
|
}
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
84
src/ripple/app/sidechain/impl/MainchainListener.h
Normal file
84
src/ripple/app/sidechain/impl/MainchainListener.h
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#ifndef RIPPLE_SIDECHAIN_IMPL_MAINCHAINLISTENER_H_INCLUDED
|
||||||
|
#define RIPPLE_SIDECHAIN_IMPL_MAINCHAINLISTENER_H_INCLUDED
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/impl/ChainListener.h>
|
||||||
|
#include <ripple/beast/utility/Journal.h>
|
||||||
|
#include <ripple/protocol/AccountID.h>
|
||||||
|
|
||||||
|
#include <boost/asio/io_service.hpp>
|
||||||
|
#include <boost/asio/ip/address.hpp>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
class Federator;
|
||||||
|
class WebsocketClient;
|
||||||
|
|
||||||
|
class MainchainListener : public ChainListener,
|
||||||
|
public std::enable_shared_from_this<MainchainListener>
|
||||||
|
{
|
||||||
|
std::unique_ptr<WebsocketClient> wsClient_;
|
||||||
|
|
||||||
|
mutable std::mutex callbacksMtx_;
|
||||||
|
std::map<std::uint32_t, RpcCallback> GUARDED_BY(callbacksMtx_) callbacks_;
|
||||||
|
|
||||||
|
void
|
||||||
|
onMessage(Json::Value const& msg) EXCLUDES(callbacksMtx_);
|
||||||
|
|
||||||
|
public:
|
||||||
|
MainchainListener(
|
||||||
|
AccountID const& account,
|
||||||
|
std::weak_ptr<Federator>&& federator,
|
||||||
|
beast::Journal j);
|
||||||
|
|
||||||
|
~MainchainListener();
|
||||||
|
|
||||||
|
void
|
||||||
|
init(
|
||||||
|
boost::asio::io_service& ios,
|
||||||
|
boost::asio::ip::address const& ip,
|
||||||
|
std::uint16_t port);
|
||||||
|
|
||||||
|
// Returns command id that will be returned in the response
|
||||||
|
std::uint32_t
|
||||||
|
send(std::string const& cmd, Json::Value const& params)
|
||||||
|
EXCLUDES(callbacksMtx_);
|
||||||
|
|
||||||
|
void
|
||||||
|
shutdown();
|
||||||
|
|
||||||
|
void
|
||||||
|
stopHistoricalTxns();
|
||||||
|
|
||||||
|
void
|
||||||
|
send(
|
||||||
|
std::string const& cmd,
|
||||||
|
Json::Value const& params,
|
||||||
|
RpcCallback onResponse) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
|
|
||||||
|
#endif
|
||||||
130
src/ripple/app/sidechain/impl/SidechainListener.cpp
Normal file
130
src/ripple/app/sidechain/impl/SidechainListener.cpp
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/impl/SidechainListener.h>
|
||||||
|
|
||||||
|
#include <ripple/app/main/Application.h>
|
||||||
|
#include <ripple/app/misc/NetworkOPs.h>
|
||||||
|
#include <ripple/app/sidechain/Federator.h>
|
||||||
|
#include <ripple/app/sidechain/FederatorEvents.h>
|
||||||
|
#include <ripple/basics/Log.h>
|
||||||
|
#include <ripple/basics/XRPAmount.h>
|
||||||
|
#include <ripple/json/Output.h>
|
||||||
|
#include <ripple/json/json_writer.h>
|
||||||
|
#include <ripple/protocol/AccountID.h>
|
||||||
|
#include <ripple/protocol/STAmount.h>
|
||||||
|
#include <ripple/protocol/jss.h>
|
||||||
|
#include <ripple/resource/Fees.h>
|
||||||
|
#include <ripple/rpc/Context.h>
|
||||||
|
#include <ripple/rpc/RPCHandler.h>
|
||||||
|
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
SidechainListener::SidechainListener(
|
||||||
|
Source& source,
|
||||||
|
AccountID const& account,
|
||||||
|
std::weak_ptr<Federator>&& federator,
|
||||||
|
Application& app,
|
||||||
|
beast::Journal j)
|
||||||
|
: InfoSub(source)
|
||||||
|
, ChainListener(
|
||||||
|
ChainListener::IsMainchain::no,
|
||||||
|
account,
|
||||||
|
std::move(federator),
|
||||||
|
j)
|
||||||
|
, app_(app)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SidechainListener::init(NetworkOPs& netOPs)
|
||||||
|
{
|
||||||
|
auto e = netOPs.subAccountHistory(shared_from_this(), doorAccount_);
|
||||||
|
if (e != rpcSUCCESS)
|
||||||
|
LogicError("Could not subscribe to side chain door account history.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SidechainListener::send(Json::Value const& msg, bool)
|
||||||
|
{
|
||||||
|
processMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SidechainListener::stopHistoricalTxns(NetworkOPs& netOPs)
|
||||||
|
{
|
||||||
|
netOPs.unsubAccountHistory(
|
||||||
|
shared_from_this(), doorAccount_, /*history only*/ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SidechainListener::send(
|
||||||
|
std::string const& cmd,
|
||||||
|
Json::Value const& params,
|
||||||
|
RpcCallback onResponse)
|
||||||
|
{
|
||||||
|
std::weak_ptr<SidechainListener> selfWeak = shared_from_this();
|
||||||
|
auto job = [cmd, params, onResponse, selfWeak]() {
|
||||||
|
auto self = selfWeak.lock();
|
||||||
|
if (!self)
|
||||||
|
return;
|
||||||
|
|
||||||
|
JLOGV(
|
||||||
|
self->j_.trace(),
|
||||||
|
"Sidechain send",
|
||||||
|
jv("command", cmd),
|
||||||
|
jv("params", params));
|
||||||
|
|
||||||
|
Json::Value const request = [&] {
|
||||||
|
Json::Value r(params);
|
||||||
|
r[jss::method] = cmd;
|
||||||
|
r[jss::jsonrpc] = "2.0";
|
||||||
|
r[jss::ripplerpc] = "2.0";
|
||||||
|
return r;
|
||||||
|
}();
|
||||||
|
Resource::Charge loadType = Resource::feeReferenceRPC;
|
||||||
|
Resource::Consumer c;
|
||||||
|
RPC::JsonContext context{
|
||||||
|
{self->j_,
|
||||||
|
self->app_,
|
||||||
|
loadType,
|
||||||
|
self->app_.getOPs(),
|
||||||
|
self->app_.getLedgerMaster(),
|
||||||
|
c,
|
||||||
|
Role::ADMIN,
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
RPC::apiMaximumSupportedVersion},
|
||||||
|
std::move(request)};
|
||||||
|
|
||||||
|
Json::Value jvResult;
|
||||||
|
RPC::doCommand(context, jvResult);
|
||||||
|
JLOG(self->j_.trace()) << "Sidechain response: " << jvResult;
|
||||||
|
if (self->app_.config().standalone())
|
||||||
|
self->app_.getOPs().acceptLedger();
|
||||||
|
onResponse(jvResult);
|
||||||
|
};
|
||||||
|
app_.getJobQueue().addJob(jtRPC, "federator rpc", job);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
|
||||||
|
} // namespace ripple
|
||||||
74
src/ripple/app/sidechain/impl/SidechainListener.h
Normal file
74
src/ripple/app/sidechain/impl/SidechainListener.h
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#ifndef RIPPLE_SIDECHAIN_IMPL_SIDECHAINLISTENER_H_INCLUDED
|
||||||
|
#define RIPPLE_SIDECHAIN_IMPL_SIDECHAINLISTENER_H_INCLUDED
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/impl/ChainListener.h>
|
||||||
|
#include <ripple/beast/utility/Journal.h>
|
||||||
|
#include <ripple/net/InfoSub.h>
|
||||||
|
#include <ripple/protocol/AccountID.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
|
||||||
|
class NetworkOPs;
|
||||||
|
class Application;
|
||||||
|
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
class Federator;
|
||||||
|
|
||||||
|
class SidechainListener : public InfoSub,
|
||||||
|
public ChainListener,
|
||||||
|
public std::enable_shared_from_this<SidechainListener>
|
||||||
|
{
|
||||||
|
Application& app_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
SidechainListener(
|
||||||
|
Source& source,
|
||||||
|
AccountID const& account,
|
||||||
|
std::weak_ptr<Federator>&& federator,
|
||||||
|
Application& app,
|
||||||
|
beast::Journal j);
|
||||||
|
|
||||||
|
void
|
||||||
|
init(NetworkOPs& netOPs);
|
||||||
|
|
||||||
|
~SidechainListener() = default;
|
||||||
|
|
||||||
|
void
|
||||||
|
send(Json::Value const& msg, bool) override;
|
||||||
|
|
||||||
|
void
|
||||||
|
stopHistoricalTxns(NetworkOPs& netOPs);
|
||||||
|
|
||||||
|
void
|
||||||
|
send(
|
||||||
|
std::string const& cmd,
|
||||||
|
Json::Value const& params,
|
||||||
|
RpcCallback onResponse) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
|
|
||||||
|
#endif
|
||||||
353
src/ripple/app/sidechain/impl/SignatureCollector.cpp
Normal file
353
src/ripple/app/sidechain/impl/SignatureCollector.cpp
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/impl/SignatureCollector.h>
|
||||||
|
|
||||||
|
#include <ripple/app/main/Application.h>
|
||||||
|
#include <ripple/app/misc/HashRouter.h>
|
||||||
|
#include <ripple/app/sidechain/Federator.h>
|
||||||
|
#include <ripple/app/sidechain/impl/SignerList.h>
|
||||||
|
#include <ripple/basics/Slice.h>
|
||||||
|
#include <ripple/core/Job.h>
|
||||||
|
#include <ripple/core/JobQueue.h>
|
||||||
|
#include <ripple/json/json_value.h>
|
||||||
|
#include <ripple/json/json_writer.h>
|
||||||
|
#include <ripple/overlay/Message.h>
|
||||||
|
#include <ripple/overlay/Overlay.h>
|
||||||
|
#include <ripple/overlay/Peer.h>
|
||||||
|
#include <ripple/protocol/STAccount.h>
|
||||||
|
#include <ripple/protocol/STObject.h>
|
||||||
|
#include <ripple/protocol/STParsedJSON.h>
|
||||||
|
#include <ripple/protocol/Serializer.h>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
std::chrono::seconds messageExpire = std::chrono::minutes{10};
|
||||||
|
|
||||||
|
uint256
|
||||||
|
computeMessageSuppression(MessageId const& mId, Slice const& signature)
|
||||||
|
{
|
||||||
|
Serializer s(128);
|
||||||
|
s.addBitString(mId);
|
||||||
|
s.addVL(signature);
|
||||||
|
return s.getSHA512Half();
|
||||||
|
}
|
||||||
|
|
||||||
|
SignatureCollector::SignatureCollector(
|
||||||
|
bool isMainChain,
|
||||||
|
SecretKey const& mySecKey,
|
||||||
|
PublicKey const& myPubKey,
|
||||||
|
beast::abstract_clock<std::chrono::steady_clock>& c,
|
||||||
|
SignerList& signers,
|
||||||
|
Federator& federator,
|
||||||
|
Application& app,
|
||||||
|
beast::Journal j)
|
||||||
|
: isMainChain_(isMainChain)
|
||||||
|
, mySecKey_(mySecKey)
|
||||||
|
, myPubKey_(myPubKey)
|
||||||
|
, messages_(c)
|
||||||
|
, signers_(signers)
|
||||||
|
, federator_(federator)
|
||||||
|
, app_(app)
|
||||||
|
, j_(j)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SignatureCollector::signAndSubmit(Json::Value const& txJson)
|
||||||
|
{
|
||||||
|
auto job = [tx = txJson,
|
||||||
|
myPK = myPubKey_,
|
||||||
|
mySK = mySecKey_,
|
||||||
|
chain =
|
||||||
|
isMainChain_ ? Federator::mainChain : Federator::sideChain,
|
||||||
|
f = federator_.weak_from_this(),
|
||||||
|
j = j_]() mutable {
|
||||||
|
auto federator = f.lock();
|
||||||
|
if (!federator)
|
||||||
|
return;
|
||||||
|
|
||||||
|
STParsedJSONObject parsed(std::string(jss::tx_json), tx);
|
||||||
|
if (parsed.object == std::nullopt)
|
||||||
|
{
|
||||||
|
JLOGV(j.fatal(), "cannot parse transaction", jv("tx", tx));
|
||||||
|
assert(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
parsed.object->setFieldVL(sfSigningPubKey, Slice(nullptr, 0));
|
||||||
|
STTx tx(std::move(parsed.object.value()));
|
||||||
|
|
||||||
|
MessageId mId{tx.getSigningHash()};
|
||||||
|
Buffer sig{tx.getMultiSignature(calcAccountID(myPK), myPK, mySK)};
|
||||||
|
federator->getSignatureCollector(chain).processSig(
|
||||||
|
mId, myPK, std::move(sig), std::move(tx));
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
JLOGV(j.fatal(), "invalid transaction", jv("tx", tx));
|
||||||
|
assert(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
app_.getJobQueue().addJob(jtFEDERATORSIGNATURE, "federator signature", job);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
SignatureCollector::processSig(
|
||||||
|
MessageId const& mId,
|
||||||
|
PublicKey const& pk,
|
||||||
|
Buffer const& sig,
|
||||||
|
std::optional<STTx> const& txOpt)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"processSig",
|
||||||
|
jv("public key", strHex(pk)),
|
||||||
|
jv("message", mId));
|
||||||
|
if (!signers_.isFederator(pk))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto valid = addSig(mId, pk, sig, txOpt);
|
||||||
|
if (txOpt)
|
||||||
|
shareSig(mId, sig);
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SignatureCollector::expire()
|
||||||
|
{
|
||||||
|
std::lock_guard lock(mtx_);
|
||||||
|
// Never expire collections with this server's sig or submitted txns
|
||||||
|
for (auto i = messages_.begin(), e = messages_.end(); i != e; ++i)
|
||||||
|
{
|
||||||
|
auto const& multiSigMsg = i->second;
|
||||||
|
if (multiSigMsg.submitted_ ||
|
||||||
|
std::any_of(
|
||||||
|
multiSigMsg.sigMaps_.begin(),
|
||||||
|
multiSigMsg.sigMaps_.end(),
|
||||||
|
[&](auto const& p) { return p.first == myPubKey_; }))
|
||||||
|
{
|
||||||
|
messages_.touch(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
beast::expire(messages_, messageExpire);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SignatureCollector::reshareSigs()
|
||||||
|
{
|
||||||
|
std::lock_guard lock(mtx_);
|
||||||
|
for (auto const& [messageId, multiSigMsg] : messages_)
|
||||||
|
{
|
||||||
|
if (multiSigMsg.submitted_)
|
||||||
|
continue;
|
||||||
|
for (auto const& [pk, sig] : multiSigMsg.sigMaps_)
|
||||||
|
{
|
||||||
|
shareSig(messageId, sig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
SignatureCollector::addSig(
|
||||||
|
MessageId const& mId,
|
||||||
|
PublicKey const& pk,
|
||||||
|
Buffer const& sig,
|
||||||
|
std::optional<STTx> const& txOpt)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"addSig",
|
||||||
|
jv("message", mId),
|
||||||
|
jv("public key", strHex(pk)),
|
||||||
|
jv("sig", strHex(sig)));
|
||||||
|
|
||||||
|
std::lock_guard lock(mtx_);
|
||||||
|
auto txi = messages_.find(mId);
|
||||||
|
if (txi == messages_.end())
|
||||||
|
{
|
||||||
|
PeerSignatureMap sigMaps;
|
||||||
|
sigMaps.emplace(pk, sig);
|
||||||
|
MultiSigMessage m{sigMaps, txOpt};
|
||||||
|
messages_.emplace(mId, std::move(m));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const verifySingle = [&](PublicKey const& pk,
|
||||||
|
Buffer const& sig) -> bool {
|
||||||
|
Serializer s;
|
||||||
|
s.add32(HashPrefix::txMultiSign);
|
||||||
|
(*txi->second.tx_).addWithoutSigningFields(s);
|
||||||
|
s.addBitString(calcAccountID(pk));
|
||||||
|
return verify(pk, s.slice(), sig, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
MultiSigMessage& message = txi->second;
|
||||||
|
if (txOpt)
|
||||||
|
{
|
||||||
|
message.tx_.emplace(std::move(*txOpt));
|
||||||
|
|
||||||
|
for (auto i = message.sigMaps_.begin(); i != message.sigMaps_.end();)
|
||||||
|
{
|
||||||
|
if (verifySingle(i->first, i->second))
|
||||||
|
++i;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"verifySingle failed",
|
||||||
|
jv("public key", strHex(i->first)));
|
||||||
|
message.sigMaps_.erase(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (message.tx_)
|
||||||
|
{
|
||||||
|
if (!verifySingle(pk, sig))
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"verifySingle failed",
|
||||||
|
jv("public key", strHex(pk)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message.sigMaps_.emplace(pk, sig);
|
||||||
|
|
||||||
|
if (!message.submitted_ && message.tx_ &&
|
||||||
|
message.sigMaps_.size() >= signers_.quorum())
|
||||||
|
{
|
||||||
|
// message.submitted_ = true;
|
||||||
|
submit(mId, lock);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SignatureCollector::shareSig(MessageId const& mId, Buffer const& sig)
|
||||||
|
{
|
||||||
|
JLOGV(j_.trace(), "shareSig", jv("message", mId), jv("sig", strHex(sig)));
|
||||||
|
|
||||||
|
std::shared_ptr<Message> toSend = [&]() -> std::shared_ptr<Message> {
|
||||||
|
protocol::TMFederatorAccountCtrlSignature m;
|
||||||
|
m.set_chain(isMainChain_ ? ::protocol::fct_MAIN : ::protocol::fct_SIDE);
|
||||||
|
m.set_publickey(myPubKey_.data(), myPubKey_.size());
|
||||||
|
m.set_messageid(mId.data(), mId.size());
|
||||||
|
m.set_signature(sig.data(), sig.size());
|
||||||
|
|
||||||
|
return std::make_shared<Message>(
|
||||||
|
m, protocol::mtFederatorAccountCtrlSignature);
|
||||||
|
}();
|
||||||
|
|
||||||
|
Overlay& overlay = app_.overlay();
|
||||||
|
HashRouter& hashRouter = app_.getHashRouter();
|
||||||
|
auto const suppression = computeMessageSuppression(mId, sig);
|
||||||
|
|
||||||
|
overlay.foreach([&](std::shared_ptr<Peer> const& p) {
|
||||||
|
hashRouter.addSuppressionPeer(suppression, p->id());
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"sending signature to peer",
|
||||||
|
jv("pid", p->id()),
|
||||||
|
jv("mid", mId));
|
||||||
|
p->send(toSend);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SignatureCollector::submit(
|
||||||
|
MessageId const& mId,
|
||||||
|
std::lock_guard<std::mutex> const&)
|
||||||
|
{
|
||||||
|
JLOGV(j_.trace(), "submit", jv("message", mId));
|
||||||
|
|
||||||
|
assert(messages_.find(mId) != messages_.end());
|
||||||
|
auto& message = messages_[mId];
|
||||||
|
assert(!message.submitted_);
|
||||||
|
message.submitted_ = true;
|
||||||
|
|
||||||
|
STArray signatures;
|
||||||
|
auto sigCount = message.sigMaps_.size();
|
||||||
|
assert(sigCount >= signers_.quorum());
|
||||||
|
signatures.reserve(sigCount);
|
||||||
|
|
||||||
|
for (auto const& item : message.sigMaps_)
|
||||||
|
{
|
||||||
|
STObject obj{sfSigner};
|
||||||
|
obj[sfAccount] = calcAccountID(item.first);
|
||||||
|
obj[sfSigningPubKey] = item.first;
|
||||||
|
obj[sfTxnSignature] = item.second;
|
||||||
|
signatures.push_back(std::move(obj));
|
||||||
|
};
|
||||||
|
|
||||||
|
std::sort(
|
||||||
|
signatures.begin(),
|
||||||
|
signatures.end(),
|
||||||
|
[](STObject const& lhs, STObject const& rhs) {
|
||||||
|
return lhs[sfAccount] < rhs[sfAccount];
|
||||||
|
});
|
||||||
|
|
||||||
|
message.tx_->setFieldArray(sfSigners, std::move(signatures));
|
||||||
|
|
||||||
|
auto sp = message.tx_->getSeqProxy();
|
||||||
|
if (sp.isTicket())
|
||||||
|
{
|
||||||
|
Json::Value r;
|
||||||
|
r[jss::tx_blob] = strHex(message.tx_->getSerializer().peekData());
|
||||||
|
|
||||||
|
JLOGV(j_.trace(), "submit", jv("tx", r));
|
||||||
|
auto callback = [&](Json::Value const& response) {
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"SignatureCollector::submit ",
|
||||||
|
jv("response", response));
|
||||||
|
};
|
||||||
|
rpcChannel_->send("submit", r, callback);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"forward to federator to submit",
|
||||||
|
jv("tx", strHex(message.tx_->getSerializer().peekData())));
|
||||||
|
federator_.addTxToSend(
|
||||||
|
(isMainChain_ ? Federator::ChainType::mainChain
|
||||||
|
: Federator::ChainType::sideChain),
|
||||||
|
sp.value(),
|
||||||
|
*(message.tx_));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SignatureCollector::setRpcChannel(std::shared_ptr<ChainListener> channel)
|
||||||
|
{
|
||||||
|
rpcChannel_ = std::move(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
149
src/ripple/app/sidechain/impl/SignatureCollector.h
Normal file
149
src/ripple/app/sidechain/impl/SignatureCollector.h
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#ifndef RIPPLE_SIDECHAIN_IMPL_SIGNATURE_COLLECTOR_H
|
||||||
|
#define RIPPLE_SIDECHAIN_IMPL_SIGNATURE_COLLECTOR_H
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/impl/ChainListener.h>
|
||||||
|
#include <ripple/basics/Buffer.h>
|
||||||
|
#include <ripple/basics/UnorderedContainers.h>
|
||||||
|
#include <ripple/beast/clock/abstract_clock.h>
|
||||||
|
#include <ripple/beast/container/aged_unordered_map.h>
|
||||||
|
#include <ripple/beast/utility/Journal.h>
|
||||||
|
#include <ripple/protocol/PublicKey.h>
|
||||||
|
#include <ripple/protocol/STTx.h>
|
||||||
|
#include <ripple/protocol/SecretKey.h>
|
||||||
|
#include <ripple.pb.h>
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
namespace Json {
|
||||||
|
class Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
|
||||||
|
class Application;
|
||||||
|
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
class SignerList;
|
||||||
|
class Federator;
|
||||||
|
|
||||||
|
using PeerSignatureMap = hash_map<PublicKey, Buffer>;
|
||||||
|
using MessageId = uint256;
|
||||||
|
struct MultiSigMessage
|
||||||
|
{
|
||||||
|
PeerSignatureMap sigMaps_;
|
||||||
|
std::optional<STTx> tx_;
|
||||||
|
bool submitted_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SignatureCollector
|
||||||
|
{
|
||||||
|
std::shared_ptr<ChainListener> rpcChannel_;
|
||||||
|
bool const isMainChain_;
|
||||||
|
SecretKey const mySecKey_;
|
||||||
|
PublicKey const myPubKey_;
|
||||||
|
mutable std::mutex mtx_;
|
||||||
|
beast::aged_unordered_map<
|
||||||
|
MessageId,
|
||||||
|
MultiSigMessage,
|
||||||
|
std::chrono::steady_clock,
|
||||||
|
beast::uhash<>>
|
||||||
|
GUARDED_BY(mtx_) messages_;
|
||||||
|
|
||||||
|
SignerList& signers_;
|
||||||
|
Federator& federator_;
|
||||||
|
Application& app_;
|
||||||
|
beast::Journal j_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
SignatureCollector(
|
||||||
|
bool isMainChain,
|
||||||
|
SecretKey const& mySecKey,
|
||||||
|
PublicKey const& myPubKey,
|
||||||
|
beast::abstract_clock<std::chrono::steady_clock>& c,
|
||||||
|
SignerList& signers,
|
||||||
|
Federator& federator,
|
||||||
|
Application& app,
|
||||||
|
beast::Journal j);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sign the tx, and share with network
|
||||||
|
* once quorum signatures are collected, the tx will be submitted
|
||||||
|
* @param tx the transaction to be signed and later submitted
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
signAndSubmit(Json::Value const& tx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* verify the signature and remember it.
|
||||||
|
* If quorum signatures are collected for the same MessageId,
|
||||||
|
* a tx will be submitted.
|
||||||
|
*
|
||||||
|
* @param mId identify the tx
|
||||||
|
* @param pk public key of signer
|
||||||
|
* @param sig signature
|
||||||
|
* @param txOpt the transaction, only used by the local node
|
||||||
|
* @return if the signature is from a federator
|
||||||
|
*/
|
||||||
|
bool
|
||||||
|
processSig(
|
||||||
|
MessageId const& mId,
|
||||||
|
PublicKey const& pk,
|
||||||
|
Buffer const& sig,
|
||||||
|
std::optional<STTx> const& txOpt);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove stale signatures
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
expire() EXCLUDES(mtx_); // TODO retry logic
|
||||||
|
|
||||||
|
void
|
||||||
|
setRpcChannel(std::shared_ptr<ChainListener> channel);
|
||||||
|
|
||||||
|
void
|
||||||
|
reshareSigs() EXCLUDES(mtx_);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// verify a signature (if it is from a peer) and add to a collection
|
||||||
|
bool
|
||||||
|
addSig(
|
||||||
|
MessageId const& mId,
|
||||||
|
PublicKey const& pk,
|
||||||
|
Buffer const& sig,
|
||||||
|
std::optional<STTx> const& txOpt) EXCLUDES(mtx_);
|
||||||
|
|
||||||
|
// share a signature to the network
|
||||||
|
void
|
||||||
|
shareSig(MessageId const& mId, Buffer const& sig);
|
||||||
|
// submit a tx since it collected quorum signatures
|
||||||
|
void
|
||||||
|
submit(MessageId const& mId, std::lock_guard<std::mutex> const&)
|
||||||
|
REQUIRES(mtx_);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
|
|
||||||
|
#endif
|
||||||
51
src/ripple/app/sidechain/impl/SignerList.cpp
Normal file
51
src/ripple/app/sidechain/impl/SignerList.cpp
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/impl/SignerList.h>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
SignerList::SignerList(
|
||||||
|
AccountID const& account,
|
||||||
|
hash_set<PublicKey> const& signers,
|
||||||
|
beast::Journal j)
|
||||||
|
: account_(account)
|
||||||
|
, signers_(signers)
|
||||||
|
, quorum_(static_cast<std::uint32_t>(std::ceil(signers.size() * 0.8)))
|
||||||
|
, j_(j)
|
||||||
|
{
|
||||||
|
(void)j_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
SignerList::isFederator(PublicKey const& pk) const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx_);
|
||||||
|
return signers_.find(pk) != signers_.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t
|
||||||
|
SignerList::quorum() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx_);
|
||||||
|
return quorum_;
|
||||||
|
}
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
63
src/ripple/app/sidechain/impl/SignerList.h
Normal file
63
src/ripple/app/sidechain/impl/SignerList.h
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#ifndef RIPPLE_SIDECHAIN_IMPL_SIGNER_LIST_H
|
||||||
|
#define RIPPLE_SIDECHAIN_IMPL_SIGNER_LIST_H
|
||||||
|
|
||||||
|
#include <ripple/basics/ThreadSaftyAnalysis.h>
|
||||||
|
#include <ripple/basics/UnorderedContainers.h>
|
||||||
|
#include <ripple/beast/utility/Journal.h>
|
||||||
|
#include <ripple/protocol/AccountID.h>
|
||||||
|
#include <ripple/protocol/PublicKey.h>
|
||||||
|
#include <ripple.pb.h>
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* grow to handle singer list changes
|
||||||
|
*/
|
||||||
|
class SignerList
|
||||||
|
{
|
||||||
|
AccountID const account_;
|
||||||
|
mutable std::mutex mtx_;
|
||||||
|
hash_set<PublicKey> GUARDED_BY(mtx_) signers_;
|
||||||
|
std::uint32_t GUARDED_BY(mtx_) quorum_;
|
||||||
|
beast::Journal j_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
SignerList(
|
||||||
|
AccountID const& account,
|
||||||
|
hash_set<PublicKey> const& signers,
|
||||||
|
beast::Journal j);
|
||||||
|
~SignerList() = default;
|
||||||
|
|
||||||
|
bool
|
||||||
|
isFederator(PublicKey const& pk) const EXCLUDES(mtx_);
|
||||||
|
|
||||||
|
std::uint32_t
|
||||||
|
quorum() const EXCLUDES(mtx_);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
|
|
||||||
|
#endif
|
||||||
791
src/ripple/app/sidechain/impl/TicketHolder.cpp
Normal file
791
src/ripple/app/sidechain/impl/TicketHolder.cpp
Normal file
@@ -0,0 +1,791 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/impl/TicketHolder.h>
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/Federator.h>
|
||||||
|
#include <ripple/app/sidechain/impl/SignatureCollector.h>
|
||||||
|
#include <ripple/basics/Log.h>
|
||||||
|
#include <ripple/basics/StringUtilities.h>
|
||||||
|
#include <ripple/json/json_writer.h>
|
||||||
|
#include <ripple/protocol/STParsedJSON.h>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
std::string
|
||||||
|
TicketPurposeToStr(TicketPurpose tp)
|
||||||
|
{
|
||||||
|
switch (tp)
|
||||||
|
{
|
||||||
|
case TicketPurpose::mainDoorKeeper:
|
||||||
|
return "mainDoorKeeper";
|
||||||
|
case TicketPurpose::sideDoorKeeper:
|
||||||
|
return "sideDoorKeeper";
|
||||||
|
case TicketPurpose::updateSignerList:
|
||||||
|
return "updateSignerList";
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
TicketHolder::TicketHolder(
|
||||||
|
bool isMainChain,
|
||||||
|
AccountID const& account,
|
||||||
|
Federator& federator,
|
||||||
|
beast::Journal j)
|
||||||
|
: isMainChain_(isMainChain)
|
||||||
|
, accountStr_(toBase58(account))
|
||||||
|
, federator_(federator)
|
||||||
|
, j_(j)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketHolder::init()
|
||||||
|
{
|
||||||
|
std::lock_guard lock(mtx_);
|
||||||
|
if (initData_.status_ != InitializeStatus::waitLedger)
|
||||||
|
return;
|
||||||
|
initData_.status_ = InitializeStatus::waitAccountObject;
|
||||||
|
rpcAccountObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::uint32_t>
|
||||||
|
TicketHolder::getTicket(TicketPurpose purpose, PeekOrTake pt)
|
||||||
|
{
|
||||||
|
std::lock_guard lock(mtx_);
|
||||||
|
|
||||||
|
if (initData_.status_ != InitializeStatus::initialized)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.debug(),
|
||||||
|
"TicketHolder getTicket but ticket holder not initialized",
|
||||||
|
jv("chain", (isMainChain_ ? "main" : "side")),
|
||||||
|
jv("purpose", TicketPurposeToStr(purpose)));
|
||||||
|
|
||||||
|
if (initData_.status_ == InitializeStatus::needToQueryTx)
|
||||||
|
rpcTx(lock);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto index = static_cast<std::underlying_type_t<TicketPurpose>>(purpose);
|
||||||
|
if (tickets_[index].status_ == AutoRenewedTicket::Status::available)
|
||||||
|
{
|
||||||
|
if (pt == PeekOrTake::take)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"getTicket",
|
||||||
|
jv("chain", (isMainChain_ ? "main" : "side")),
|
||||||
|
jv("seq", tickets_[index].seq_));
|
||||||
|
tickets_[index].status_ = AutoRenewedTicket::Status::taken;
|
||||||
|
}
|
||||||
|
return tickets_[index].seq_;
|
||||||
|
}
|
||||||
|
if (pt == PeekOrTake::take)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"getTicket",
|
||||||
|
jv("chain", (isMainChain_ ? "main" : "side")),
|
||||||
|
jv("no ticket for ", TicketPurposeToStr(purpose)));
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketHolder::onEvent(event::TicketCreateResult const& e)
|
||||||
|
{
|
||||||
|
std::lock_guard lock(mtx_);
|
||||||
|
if (initData_.status_ != InitializeStatus::initialized)
|
||||||
|
{
|
||||||
|
JLOG(j_.trace()) << "TicketHolder queues an event";
|
||||||
|
initData_.toReplay_.emplace(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
processEvent(e, lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketHolder::onEvent(event::BootstrapTicket const& e)
|
||||||
|
{
|
||||||
|
std::lock_guard lock(mtx_);
|
||||||
|
if (initData_.status_ != InitializeStatus::initialized)
|
||||||
|
{
|
||||||
|
JLOG(j_.trace()) << "TicketHolder queues an event";
|
||||||
|
initData_.bootstrapTicketToReplay_.emplace(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
processEvent(e, lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
TicketHolder::getInfo() const
|
||||||
|
{
|
||||||
|
Json::Value ret{Json::objectValue};
|
||||||
|
{
|
||||||
|
std::lock_guard lock{mtx_};
|
||||||
|
if (initData_.status_ == InitializeStatus::initialized)
|
||||||
|
{
|
||||||
|
ret["initialized"] = "true";
|
||||||
|
Json::Value tickets{Json::arrayValue};
|
||||||
|
for (auto const& t : tickets_)
|
||||||
|
{
|
||||||
|
Json::Value tj{Json::objectValue};
|
||||||
|
tj["ticket_seq"] = t.seq_;
|
||||||
|
tj["status"] = t.status_ == AutoRenewedTicket::Status::taken
|
||||||
|
? "taken"
|
||||||
|
: "available";
|
||||||
|
tickets.append(tj);
|
||||||
|
}
|
||||||
|
ret["tickets"] = tickets;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ret["initialized"] = "false";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketHolder::rpcAccountObject()
|
||||||
|
{
|
||||||
|
Json::Value params = [&] {
|
||||||
|
Json::Value r;
|
||||||
|
r[jss::account] = accountStr_;
|
||||||
|
r[jss::ledger_index] = "validated";
|
||||||
|
r[jss::type] = "ticket";
|
||||||
|
r[jss::limit] = 250;
|
||||||
|
return r;
|
||||||
|
}();
|
||||||
|
|
||||||
|
rpcChannel_->send(
|
||||||
|
"account_objects",
|
||||||
|
params,
|
||||||
|
[isMainChain = isMainChain_,
|
||||||
|
f = federator_.weak_from_this()](Json::Value const& response) {
|
||||||
|
auto federator = f.lock();
|
||||||
|
if (!federator)
|
||||||
|
return;
|
||||||
|
federator->getTicketRunner().accountObjectResult(
|
||||||
|
isMainChain, response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketHolder::accountObjectResult(Json::Value const& rpcResult)
|
||||||
|
{
|
||||||
|
auto ledgerNAccountObjectOpt =
|
||||||
|
[&]() -> std::optional<std::pair<std::uint32_t, Json::Value>> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (rpcResult.isMember(jss::error))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (!rpcResult[jss::validated].asBool())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (rpcResult[jss::account] != accountStr_)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (!rpcResult[jss::ledger_index].isIntegral())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (!rpcResult.isMember(jss::account_objects) ||
|
||||||
|
!rpcResult[jss::account_objects].isArray())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return std::make_pair(
|
||||||
|
rpcResult[jss::ledger_index].asUInt(),
|
||||||
|
rpcResult[jss::account_objects]);
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (!ledgerNAccountObjectOpt)
|
||||||
|
{
|
||||||
|
// can reach here?
|
||||||
|
// should not since we only ask account_object after a validated ledger
|
||||||
|
JLOGV(j_.error(), "AccountObject", jv("result", rpcResult));
|
||||||
|
assert(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& [ledgerIndex, accountObjects] = *ledgerNAccountObjectOpt;
|
||||||
|
std::lock_guard<std::mutex> lock(mtx_);
|
||||||
|
if (initData_.status_ != InitializeStatus::waitAccountObject)
|
||||||
|
{
|
||||||
|
JLOG(j_.warn()) << "unexpected AccountObject";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initData_.ledgerIndex_ = ledgerIndex;
|
||||||
|
for (auto const& o : accountObjects)
|
||||||
|
{
|
||||||
|
if (!o.isMember("LedgerEntryType") ||
|
||||||
|
o["LedgerEntryType"] != jss::Ticket)
|
||||||
|
continue;
|
||||||
|
// the following fields are mandatory
|
||||||
|
uint256 txHash;
|
||||||
|
if (!txHash.parseHex(o["PreviousTxnID"].asString()))
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.error(),
|
||||||
|
"AccountObject cannot parse tx hash",
|
||||||
|
jv("result", rpcResult));
|
||||||
|
assert(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::uint32_t ticketSeq = o["TicketSequence"].asUInt();
|
||||||
|
if (initData_.tickets_.find(txHash) != initData_.tickets_.end())
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.error(),
|
||||||
|
"AccountObject cannot parse tx hash",
|
||||||
|
jv("result", rpcResult));
|
||||||
|
assert(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initData_.tickets_.emplace(txHash, ticketSeq);
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"AccountObject, add",
|
||||||
|
jv("tx hash", txHash),
|
||||||
|
jv("ticketSeq", ticketSeq));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initData_.tickets_.empty())
|
||||||
|
{
|
||||||
|
JLOG(j_.debug()) << "Door account has no tickets in current ledger, "
|
||||||
|
"unlikely but could happen";
|
||||||
|
replay(lock);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rpcTx(lock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketHolder::rpcTx(std::lock_guard<std::mutex> const&)
|
||||||
|
{
|
||||||
|
assert(!initData_.tickets_.empty());
|
||||||
|
initData_.status_ = InitializeStatus::waitTx;
|
||||||
|
for (auto const& t : initData_.tickets_)
|
||||||
|
{
|
||||||
|
JLOG(j_.trace()) << "TicketHolder query tx " << t.first;
|
||||||
|
Json::Value params;
|
||||||
|
params[jss::transaction] = strHex(t.first);
|
||||||
|
rpcChannel_->send(
|
||||||
|
"tx",
|
||||||
|
params,
|
||||||
|
[isMainChain = isMainChain_,
|
||||||
|
f = federator_.weak_from_this()](Json::Value const& response) {
|
||||||
|
auto federator = f.lock();
|
||||||
|
if (!federator)
|
||||||
|
return;
|
||||||
|
federator->getTicketRunner().txResult(isMainChain, response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketHolder::txResult(Json::Value const& rpcResult)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx_);
|
||||||
|
if (initData_.status_ != InitializeStatus::waitTx &&
|
||||||
|
initData_.status_ != InitializeStatus::needToQueryTx)
|
||||||
|
return;
|
||||||
|
auto txOpt = [&]() -> std::optional<std::pair<TicketPurpose, uint256>> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (rpcResult.isMember(jss::error))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (rpcResult[jss::Account] != accountStr_)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rpcResult[jss::TransactionType] != "TicketCreate")
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcResult["SourceTag"].isIntegral())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
std::uint32_t tp = rpcResult["SourceTag"].asUInt();
|
||||||
|
if (tp >= static_cast<std::underlying_type_t<TicketPurpose>>(
|
||||||
|
TicketPurpose::TP_NumberOfItems))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 txHash;
|
||||||
|
if (!txHash.parseHex(rpcResult[jss::hash].asString()))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return std::make_pair(static_cast<TicketPurpose>(tp), txHash);
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (!txOpt)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.warn(),
|
||||||
|
"TicketCreate can not be found or has wrong format",
|
||||||
|
jv("result", rpcResult));
|
||||||
|
if (initData_.status_ == InitializeStatus::waitTx)
|
||||||
|
initData_.status_ = InitializeStatus::needToQueryTx;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [tPurpose, txHash] = *txOpt;
|
||||||
|
if (initData_.tickets_.find(txHash) == initData_.tickets_.end())
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.debug(),
|
||||||
|
"Repeated TicketCreate tx result",
|
||||||
|
jv("result", rpcResult));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& ticket = initData_.tickets_[txHash];
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"TicketHolder txResult",
|
||||||
|
jv("purpose", TicketPurposeToStr(tPurpose)),
|
||||||
|
jv("txHash", txHash));
|
||||||
|
|
||||||
|
auto index = static_cast<std::underlying_type_t<TicketPurpose>>(tPurpose);
|
||||||
|
tickets_[index].seq_ = ticket;
|
||||||
|
tickets_[index].status_ = AutoRenewedTicket::Status::available;
|
||||||
|
initData_.tickets_.erase(txHash);
|
||||||
|
|
||||||
|
if (initData_.tickets_.empty())
|
||||||
|
{
|
||||||
|
replay(lock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketHolder::replay(std::lock_guard<std::mutex> const& lock)
|
||||||
|
{
|
||||||
|
assert(initData_.tickets_.empty());
|
||||||
|
// replay bootstrap tickets first if any
|
||||||
|
while (!initData_.bootstrapTicketToReplay_.empty())
|
||||||
|
{
|
||||||
|
auto e = initData_.bootstrapTicketToReplay_.front();
|
||||||
|
processEvent(e, lock);
|
||||||
|
initData_.bootstrapTicketToReplay_.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!initData_.toReplay_.empty())
|
||||||
|
{
|
||||||
|
auto e = initData_.toReplay_.front();
|
||||||
|
processEvent(e, lock);
|
||||||
|
initData_.toReplay_.pop();
|
||||||
|
}
|
||||||
|
initData_.status_ = InitializeStatus::initialized;
|
||||||
|
JLOG(j_.info()) << "TicketHolder initialized";
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class E>
|
||||||
|
void
|
||||||
|
TicketHolder::processEvent(E const& e, std::lock_guard<std::mutex> const&)
|
||||||
|
{
|
||||||
|
std::uint32_t const tSeq = e.txnSeq_ + 1;
|
||||||
|
if (e.sourceTag_ >= static_cast<std::underlying_type_t<TicketPurpose>>(
|
||||||
|
TicketPurpose::TP_NumberOfItems))
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.error(),
|
||||||
|
"Wrong sourceTag",
|
||||||
|
jv("chain", (isMainChain_ ? "main" : "side")),
|
||||||
|
jv("sourceTag", e.sourceTag_));
|
||||||
|
assert(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto purposeStr =
|
||||||
|
TicketPurposeToStr(static_cast<TicketPurpose>(e.sourceTag_));
|
||||||
|
|
||||||
|
if (e.ledgerIndex_ <= initData_.ledgerIndex_)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"TicketHolder, ignoring an old ticket",
|
||||||
|
jv("chain", (isMainChain_ ? "main" : "side")),
|
||||||
|
jv("ticket seq", tSeq),
|
||||||
|
jv("purpose", purposeStr));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!e.success_)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.error(),
|
||||||
|
"CreateTicket failed",
|
||||||
|
jv("chain", (isMainChain_ ? "main" : "side")),
|
||||||
|
jv("ticket seq", tSeq),
|
||||||
|
jv("purpose", purposeStr));
|
||||||
|
assert(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"TicketHolder, got a ticket",
|
||||||
|
jv("chain", (isMainChain_ ? "main" : "side")),
|
||||||
|
jv("ticket seq", tSeq),
|
||||||
|
jv("purpose", purposeStr));
|
||||||
|
|
||||||
|
std::uint32_t const ticketPurposeToIndex = e.sourceTag_;
|
||||||
|
|
||||||
|
if (e.eventType() == event::EventType::bootstrap &&
|
||||||
|
tickets_[ticketPurposeToIndex].seq_ != 0)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.error(),
|
||||||
|
"Got a bootstrap ticket too late",
|
||||||
|
jv("chain", (isMainChain_ ? "main" : "side")),
|
||||||
|
jv("ticket seq", tSeq),
|
||||||
|
jv("purpose", purposeStr));
|
||||||
|
assert(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tickets_[ticketPurposeToIndex].seq_ = tSeq;
|
||||||
|
tickets_[ticketPurposeToIndex].status_ =
|
||||||
|
AutoRenewedTicket::Status::available;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketHolder::setRpcChannel(std::shared_ptr<ChainListener> channel)
|
||||||
|
{
|
||||||
|
rpcChannel_ = std::move(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
TicketRunner::TicketRunner(
|
||||||
|
const AccountID& mainAccount,
|
||||||
|
const AccountID& sideAccount,
|
||||||
|
Federator& federator,
|
||||||
|
beast::Journal j)
|
||||||
|
: mainAccountStr_(toBase58(mainAccount))
|
||||||
|
, sideAccountStr_(toBase58(sideAccount))
|
||||||
|
, federator_(federator)
|
||||||
|
, mainHolder_(true, mainAccount, federator, j)
|
||||||
|
, sideHolder_(false, sideAccount, federator, j)
|
||||||
|
, j_(j)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketRunner::setRpcChannel(
|
||||||
|
bool isMainChain,
|
||||||
|
std::shared_ptr<ChainListener> channel)
|
||||||
|
{
|
||||||
|
if (isMainChain)
|
||||||
|
mainHolder_.setRpcChannel(std::move(channel));
|
||||||
|
else
|
||||||
|
sideHolder_.setRpcChannel(std::move(channel));
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketRunner::init(bool isMainChain)
|
||||||
|
{
|
||||||
|
if (isMainChain)
|
||||||
|
mainHolder_.init();
|
||||||
|
else
|
||||||
|
sideHolder_.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketRunner::accountObjectResult(
|
||||||
|
bool isMainChain,
|
||||||
|
Json::Value const& rpcResult)
|
||||||
|
{
|
||||||
|
if (isMainChain)
|
||||||
|
mainHolder_.accountObjectResult(rpcResult);
|
||||||
|
else
|
||||||
|
sideHolder_.accountObjectResult(rpcResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketRunner::txResult(bool isMainChain, Json::Value const& rpcResult)
|
||||||
|
{
|
||||||
|
if (isMainChain)
|
||||||
|
mainHolder_.txResult(rpcResult);
|
||||||
|
else
|
||||||
|
sideHolder_.txResult(rpcResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
TicketRunner::trigger(
|
||||||
|
TicketPurpose purpose,
|
||||||
|
std::optional<Json::Value> const& mainTxJson,
|
||||||
|
std::optional<Json::Value> const& sideTxJson)
|
||||||
|
{
|
||||||
|
if (!mainTxJson && !sideTxJson)
|
||||||
|
{
|
||||||
|
assert(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ticketPair =
|
||||||
|
[&]() -> std::optional<std::pair<std::uint32_t, std::uint32_t>> {
|
||||||
|
std::lock_guard<std::mutex> lock(mtx_);
|
||||||
|
if (!mainHolder_.getTicket(purpose, TicketHolder::PeekOrTake::peek) ||
|
||||||
|
!sideHolder_.getTicket(purpose, TicketHolder::PeekOrTake::peek))
|
||||||
|
{
|
||||||
|
JLOG(j_.trace()) << "TicketRunner tickets no ready";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
auto mainTicket =
|
||||||
|
mainHolder_.getTicket(purpose, TicketHolder::PeekOrTake::take);
|
||||||
|
auto sideTicket =
|
||||||
|
sideHolder_.getTicket(purpose, TicketHolder::PeekOrTake::take);
|
||||||
|
assert(mainTicket && sideTicket);
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"TicketRunner trigger",
|
||||||
|
jv("main ticket", *mainTicket),
|
||||||
|
jv("side ticket", *sideTicket),
|
||||||
|
jv("purpose", TicketPurposeToStr(purpose)));
|
||||||
|
return {{*mainTicket, *sideTicket}};
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (!ticketPair)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto sendTriggerTx = [&](std::string const& accountStr,
|
||||||
|
std::uint32_t ticketSequence,
|
||||||
|
std::optional<Json::Value> const& memoJson,
|
||||||
|
SignatureCollector& signatureCollector) {
|
||||||
|
XRPAmount const fee{Federator::accountControlTxFee};
|
||||||
|
Json::Value txJson;
|
||||||
|
txJson[jss::TransactionType] = "AccountSet";
|
||||||
|
txJson[jss::Account] = accountStr;
|
||||||
|
txJson[jss::Sequence] = 0;
|
||||||
|
txJson[jss::Fee] = to_string(fee);
|
||||||
|
txJson["SourceTag"] =
|
||||||
|
static_cast<std::underlying_type_t<TicketPurpose>>(purpose);
|
||||||
|
txJson["TicketSequence"] = ticketSequence;
|
||||||
|
if (memoJson)
|
||||||
|
{
|
||||||
|
Serializer s;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
STParsedJSONObject parsed(std::string(jss::tx_json), *memoJson);
|
||||||
|
if (!parsed.object)
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j_.fatal(), "invalid transaction", jv("tx", *memoJson));
|
||||||
|
assert(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parsed.object->setFieldVL(sfSigningPubKey, Slice(nullptr, 0));
|
||||||
|
parsed.object->add(s);
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
JLOGV(j_.fatal(), "invalid transaction", jv("tx", *memoJson));
|
||||||
|
assert(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value memos{Json::arrayValue};
|
||||||
|
Json::Value memo;
|
||||||
|
auto const dataStr = strHex(s.peekData());
|
||||||
|
memo[jss::Memo][jss::MemoData] = dataStr;
|
||||||
|
memos.append(memo);
|
||||||
|
txJson[jss::Memos] = memos;
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"TicketRunner",
|
||||||
|
jv("tx", *memoJson),
|
||||||
|
jv("tx packed", dataStr),
|
||||||
|
jv("packed size", dataStr.length()));
|
||||||
|
assert(
|
||||||
|
memo[jss::Memo][jss::MemoData].asString().length() <=
|
||||||
|
event::MemoStringMax);
|
||||||
|
}
|
||||||
|
signatureCollector.signAndSubmit(txJson);
|
||||||
|
};
|
||||||
|
|
||||||
|
sendTriggerTx(
|
||||||
|
mainAccountStr_,
|
||||||
|
ticketPair->first,
|
||||||
|
mainTxJson,
|
||||||
|
federator_.getSignatureCollector(Federator::ChainType::mainChain));
|
||||||
|
sendTriggerTx(
|
||||||
|
sideAccountStr_,
|
||||||
|
ticketPair->second,
|
||||||
|
sideTxJson,
|
||||||
|
federator_.getSignatureCollector(Federator::ChainType::sideChain));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketRunner::onEvent(
|
||||||
|
std::uint32_t accountSeq,
|
||||||
|
const event::TicketCreateTrigger& e)
|
||||||
|
{
|
||||||
|
Json::Value txJson;
|
||||||
|
XRPAmount const fee{Federator::accountControlTxFee};
|
||||||
|
txJson[jss::TransactionType] = "TicketCreate";
|
||||||
|
txJson[jss::Account] =
|
||||||
|
e.dir_ == event::Dir::mainToSide ? sideAccountStr_ : mainAccountStr_;
|
||||||
|
txJson[jss::Sequence] = accountSeq;
|
||||||
|
txJson[jss::Fee] = to_string(fee);
|
||||||
|
txJson["TicketCount"] = 1;
|
||||||
|
txJson["SourceTag"] = e.sourceTag_;
|
||||||
|
{
|
||||||
|
Json::Value memos{Json::arrayValue};
|
||||||
|
{
|
||||||
|
Json::Value memo;
|
||||||
|
memo[jss::Memo][jss::MemoData] = to_string(e.txnHash_);
|
||||||
|
memos.append(memo);
|
||||||
|
}
|
||||||
|
if (!e.memoStr_.empty())
|
||||||
|
{
|
||||||
|
Json::Value memo;
|
||||||
|
memo[jss::Memo][jss::MemoData] = e.memoStr_;
|
||||||
|
memos.append(memo);
|
||||||
|
}
|
||||||
|
txJson[jss::Memos] = memos;
|
||||||
|
}
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"TicketRunner TicketTriggerDetected",
|
||||||
|
jv("chain", (e.dir_ == event::Dir::mainToSide ? "main" : "side")),
|
||||||
|
jv("seq", accountSeq),
|
||||||
|
jv("CreateTicket tx", txJson));
|
||||||
|
|
||||||
|
if (e.dir_ == event::Dir::mainToSide)
|
||||||
|
federator_.getSignatureCollector(Federator::ChainType::sideChain)
|
||||||
|
.signAndSubmit(txJson);
|
||||||
|
else
|
||||||
|
federator_.getSignatureCollector(Federator::ChainType::mainChain)
|
||||||
|
.signAndSubmit(txJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketRunner::onEvent(
|
||||||
|
std::uint32_t accountSeq,
|
||||||
|
const event::TicketCreateResult& e)
|
||||||
|
{
|
||||||
|
auto const [fromChain, toChain] = e.dir_ == event::Dir::mainToSide
|
||||||
|
? std::make_pair(Federator::sideChain, Federator::mainChain)
|
||||||
|
: std::make_pair(Federator::mainChain, Federator::sideChain);
|
||||||
|
|
||||||
|
auto ticketSeq = e.txnSeq_ + 1;
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"TicketRunner CreateTicketResult",
|
||||||
|
jv("chain",
|
||||||
|
(fromChain == Federator::ChainType::mainChain ? "main" : "side")),
|
||||||
|
jv("ticket seq", ticketSeq));
|
||||||
|
|
||||||
|
if (fromChain == Federator::ChainType::mainChain)
|
||||||
|
mainHolder_.onEvent(e);
|
||||||
|
else
|
||||||
|
sideHolder_.onEvent(e);
|
||||||
|
|
||||||
|
federator_.addSeqToSkip(fromChain, ticketSeq);
|
||||||
|
|
||||||
|
if (accountSeq)
|
||||||
|
{
|
||||||
|
assert(!e.memoStr_.empty());
|
||||||
|
auto txData = strUnHex(e.memoStr_);
|
||||||
|
if (!txData || !txData->size())
|
||||||
|
{
|
||||||
|
assert(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SerialIter sitTrans(makeSlice(*txData));
|
||||||
|
STTx tx(sitTrans);
|
||||||
|
tx.setFieldU32(sfSequence, accountSeq);
|
||||||
|
|
||||||
|
auto txJson = tx.getJson(JsonOptions::none);
|
||||||
|
// trigger hash
|
||||||
|
Json::Value memos{Json::arrayValue};
|
||||||
|
{
|
||||||
|
Json::Value memo;
|
||||||
|
memo[jss::Memo][jss::MemoData] = to_string(e.txnHash_);
|
||||||
|
memos.append(memo);
|
||||||
|
}
|
||||||
|
txJson[jss::Memos] = memos;
|
||||||
|
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"TicketRunner AccountControlTrigger",
|
||||||
|
jv("chain",
|
||||||
|
(toChain == Federator::ChainType::mainChain ? "side" : "main")),
|
||||||
|
jv("tx with added memos", txJson.toStyledString()));
|
||||||
|
|
||||||
|
federator_.getSignatureCollector(toChain).signAndSubmit(txJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TicketRunner::onEvent(const event::BootstrapTicket& e)
|
||||||
|
{
|
||||||
|
auto ticketSeq = e.txnSeq_ + 1;
|
||||||
|
JLOGV(
|
||||||
|
j_.trace(),
|
||||||
|
"TicketRunner BootstrapTicket",
|
||||||
|
jv("chain", (e.isMainchain_ ? "main" : "side")),
|
||||||
|
jv("ticket seq", ticketSeq));
|
||||||
|
|
||||||
|
if (e.isMainchain_)
|
||||||
|
mainHolder_.onEvent(e);
|
||||||
|
else
|
||||||
|
sideHolder_.onEvent(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
TicketRunner::getInfo(bool isMainchain) const
|
||||||
|
{
|
||||||
|
if (isMainchain)
|
||||||
|
return mainHolder_.getInfo();
|
||||||
|
else
|
||||||
|
return sideHolder_.getInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
255
src/ripple/app/sidechain/impl/TicketHolder.h
Normal file
255
src/ripple/app/sidechain/impl/TicketHolder.h
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#ifndef RIPPLE_SIDECHAIN_IMPL_TICKET_BOOTH_H
|
||||||
|
#define RIPPLE_SIDECHAIN_IMPL_TICKET_BOOTH_H
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/FederatorEvents.h>
|
||||||
|
#include <ripple/app/sidechain/impl/ChainListener.h>
|
||||||
|
#include <ripple/basics/UnorderedContainers.h>
|
||||||
|
#include <ripple/basics/base_uint.h>
|
||||||
|
#include <ripple/beast/utility/Journal.h>
|
||||||
|
#include <ripple/json/json_value.h>
|
||||||
|
#include <ripple/protocol/AccountID.h>
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <queue>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
class Federator;
|
||||||
|
|
||||||
|
enum class TicketPurpose : std::uint32_t {
|
||||||
|
mainDoorKeeper,
|
||||||
|
sideDoorKeeper,
|
||||||
|
updateSignerList,
|
||||||
|
TP_NumberOfItems
|
||||||
|
};
|
||||||
|
std::string
|
||||||
|
TicketPurposeToStr(TicketPurpose tp);
|
||||||
|
|
||||||
|
struct AutoRenewedTicket
|
||||||
|
{
|
||||||
|
enum class Status { available, taken };
|
||||||
|
|
||||||
|
std::uint32_t seq_;
|
||||||
|
Status status_;
|
||||||
|
|
||||||
|
AutoRenewedTicket() : seq_(0), status_(Status::taken)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class TicketHolder
|
||||||
|
{
|
||||||
|
enum class InitializeStatus {
|
||||||
|
waitLedger,
|
||||||
|
waitAccountObject,
|
||||||
|
waitTx,
|
||||||
|
needToQueryTx,
|
||||||
|
initialized
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InitializeData
|
||||||
|
{
|
||||||
|
InitializeStatus status_ = InitializeStatus::waitLedger;
|
||||||
|
hash_map<uint256, std::uint32_t> tickets_;
|
||||||
|
std::queue<event::TicketCreateResult> toReplay_;
|
||||||
|
std::queue<event::BootstrapTicket> bootstrapTicketToReplay_;
|
||||||
|
std::uint32_t ledgerIndex_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::shared_ptr<ChainListener> rpcChannel_;
|
||||||
|
bool isMainChain_;
|
||||||
|
std::string const accountStr_;
|
||||||
|
AutoRenewedTicket
|
||||||
|
tickets_[static_cast<std::underlying_type_t<TicketPurpose>>(
|
||||||
|
TicketPurpose::TP_NumberOfItems)];
|
||||||
|
InitializeData initData_;
|
||||||
|
Federator& federator_;
|
||||||
|
beast::Journal j_;
|
||||||
|
mutable std::mutex mtx_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
TicketHolder(
|
||||||
|
bool isMainChain,
|
||||||
|
AccountID const& account,
|
||||||
|
Federator& federator,
|
||||||
|
beast::Journal j);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* start to initialize the ticketHolder by sending accountObject RPC
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
init() EXCLUDES(mtx_);
|
||||||
|
/**
|
||||||
|
* process accountObject result and find the tickets.
|
||||||
|
* Initialization is not completed, because a ticket ledger object
|
||||||
|
* does not have information about its purpose.
|
||||||
|
* The purpose is in the TicketCreate tx what created the ticket.
|
||||||
|
* So the ticketHolder queries the TicketCreate tx for each ticket found.
|
||||||
|
* @param rpcResult accountObject result
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
accountObjectResult(Json::Value const& rpcResult) EXCLUDES(mtx_);
|
||||||
|
/**
|
||||||
|
* process tx RPC result
|
||||||
|
* Initialization is completed once all TicketCreate txns are found,
|
||||||
|
* one for every ticket found in the previous initialization step.
|
||||||
|
* @param rpcResult tx result
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
txResult(Json::Value const& rpcResult) EXCLUDES(mtx_);
|
||||||
|
|
||||||
|
enum class PeekOrTake { peek, take };
|
||||||
|
/**
|
||||||
|
* take or peek the ticket for a purpose
|
||||||
|
* @param purpose the ticket purpose
|
||||||
|
* @param pt take or peek
|
||||||
|
* @return the ticket if exist and not taken
|
||||||
|
*/
|
||||||
|
std::optional<std::uint32_t>
|
||||||
|
getTicket(TicketPurpose purpose, PeekOrTake pt) EXCLUDES(mtx_);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* process a TicketCreateResult event, update the ticket number and status
|
||||||
|
* It queues the event if the ticketHolder is not yet initialized.
|
||||||
|
*
|
||||||
|
* @param e the TicketCreateResult event
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
onEvent(event::TicketCreateResult const& e) EXCLUDES(mtx_);
|
||||||
|
/**
|
||||||
|
* process a ticket created during network bootstrap
|
||||||
|
* @param e the BootstrapTicket event
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
onEvent(event::BootstrapTicket const& e) EXCLUDES(mtx_);
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
getInfo() const EXCLUDES(mtx_);
|
||||||
|
|
||||||
|
void
|
||||||
|
setRpcChannel(std::shared_ptr<ChainListener> channel);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void
|
||||||
|
rpcAccountObject();
|
||||||
|
|
||||||
|
void
|
||||||
|
rpcTx(std::lock_guard<std::mutex> const&) REQUIRES(mtx_);
|
||||||
|
|
||||||
|
// replay accumulated events before finish initialization
|
||||||
|
void
|
||||||
|
replay(std::lock_guard<std::mutex> const&) REQUIRES(mtx_);
|
||||||
|
template <class E>
|
||||||
|
void
|
||||||
|
processEvent(E const& e, std::lock_guard<std::mutex> const&) REQUIRES(mtx_);
|
||||||
|
};
|
||||||
|
|
||||||
|
class TicketRunner
|
||||||
|
{
|
||||||
|
std::string const mainAccountStr_;
|
||||||
|
std::string const sideAccountStr_;
|
||||||
|
Federator& federator_;
|
||||||
|
TicketHolder mainHolder_;
|
||||||
|
TicketHolder sideHolder_;
|
||||||
|
beast::Journal j_;
|
||||||
|
// Only one thread at a time can grab tickets
|
||||||
|
mutable std::mutex mtx_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
TicketRunner(
|
||||||
|
AccountID const& mainAccount,
|
||||||
|
AccountID const& sideAccount,
|
||||||
|
Federator& federator,
|
||||||
|
beast::Journal j);
|
||||||
|
|
||||||
|
// set RpcChannel for a ticketHolder
|
||||||
|
void
|
||||||
|
setRpcChannel(bool isMainChain, std::shared_ptr<ChainListener> channel);
|
||||||
|
// init a ticketHolder
|
||||||
|
void
|
||||||
|
init(bool isMainChain);
|
||||||
|
// pass a accountObject RPC result to a ticketHolder
|
||||||
|
void
|
||||||
|
accountObjectResult(bool isMainChain, Json::Value const& rpcResult);
|
||||||
|
// pass a tx RPC result to a ticketHolder
|
||||||
|
void
|
||||||
|
txResult(bool isMainChain, Json::Value const& rpcResult);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start to run a protocol that submit a federator account control tx
|
||||||
|
* to the network.
|
||||||
|
*
|
||||||
|
* Comparing to a normal tx submission that takes one step, a federator
|
||||||
|
* account control tx (such as depositAuth and signerListSet) takes 3 steps:
|
||||||
|
* 1. use a ticket to send a accountSet no-op tx as a trigger
|
||||||
|
* 2. create a new ticket
|
||||||
|
* 3. submit the account control tx
|
||||||
|
*
|
||||||
|
* @param ticketPurpose the purpose of ticket. The purpose describes
|
||||||
|
* the account control tx use case.
|
||||||
|
* @param mainTxJson account control tx for main chain
|
||||||
|
* @param sideTxJson account control tx for side chain
|
||||||
|
* @note mainTxJson and sideTxJson cannot both be empty
|
||||||
|
* @return if the protocol started
|
||||||
|
*/
|
||||||
|
[[nodiscard]] bool
|
||||||
|
trigger(
|
||||||
|
TicketPurpose ticketPurpose,
|
||||||
|
std::optional<Json::Value> const& mainTxJson,
|
||||||
|
std::optional<Json::Value> const& sideTxJson) EXCLUDES(mtx_);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* process a TicketCreateTrigger event, by submitting TicketCreate tx
|
||||||
|
*
|
||||||
|
* This event is generated when the accountSet no-op tx
|
||||||
|
* (as the protocol trigger) appears in the tx stream,
|
||||||
|
* i.e. sorted with regular XChain payments.
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
onEvent(std::uint32_t accountSeq, event::TicketCreateTrigger const& e);
|
||||||
|
/**
|
||||||
|
* process a TicketCreateResult event, update the ticketHolder.
|
||||||
|
*
|
||||||
|
* This event is generated when the TicketCreate tx appears
|
||||||
|
* in the tx stream.
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
onEvent(std::uint32_t accountSeq, event::TicketCreateResult const& e);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* process a ticket created during network bootstrap
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
onEvent(event::BootstrapTicket const& e);
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
getInfo(bool isMainchain) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
|
|
||||||
|
#endif
|
||||||
177
src/ripple/app/sidechain/impl/WebsocketClient.cpp
Normal file
177
src/ripple/app/sidechain/impl/WebsocketClient.cpp
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2016 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#include <ripple/app/sidechain/impl/WebsocketClient.h>
|
||||||
|
|
||||||
|
#include <ripple/basics/Log.h>
|
||||||
|
#include <ripple/json/Output.h>
|
||||||
|
#include <ripple/json/json_reader.h>
|
||||||
|
#include <ripple/json/json_writer.h>
|
||||||
|
#include <ripple/json/to_string.h>
|
||||||
|
#include <ripple/protocol/jss.h>
|
||||||
|
#include <ripple/server/Port.h>
|
||||||
|
|
||||||
|
#include <boost/beast/websocket.hpp>
|
||||||
|
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
template <class ConstBuffers>
|
||||||
|
std::string
|
||||||
|
WebsocketClient::buffer_string(ConstBuffers const& b)
|
||||||
|
{
|
||||||
|
using boost::asio::buffer;
|
||||||
|
using boost::asio::buffer_size;
|
||||||
|
std::string s;
|
||||||
|
s.resize(buffer_size(b));
|
||||||
|
buffer_copy(buffer(&s[0], s.size()), b);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
WebsocketClient::cleanup()
|
||||||
|
{
|
||||||
|
ios_.post(strand_.wrap([this] {
|
||||||
|
if (!peerClosed_)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
ws_.async_close({}, strand_.wrap([&](error_code ec) {
|
||||||
|
stream_.cancel(ec);
|
||||||
|
|
||||||
|
std::lock_guard l(shutdownM_);
|
||||||
|
isShutdown_ = true;
|
||||||
|
shutdownCv_.notify_one();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> l(shutdownM_);
|
||||||
|
isShutdown_ = true;
|
||||||
|
shutdownCv_.notify_one();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
WebsocketClient::shutdown()
|
||||||
|
{
|
||||||
|
cleanup();
|
||||||
|
std::unique_lock l{shutdownM_};
|
||||||
|
shutdownCv_.wait(l, [this] { return isShutdown_; });
|
||||||
|
}
|
||||||
|
|
||||||
|
WebsocketClient::WebsocketClient(
|
||||||
|
std::function<void(Json::Value const&)> callback,
|
||||||
|
boost::asio::io_service& ios,
|
||||||
|
boost::asio::ip::address const& ip,
|
||||||
|
std::uint16_t port,
|
||||||
|
std::unordered_map<std::string, std::string> const& headers,
|
||||||
|
beast::Journal j)
|
||||||
|
: ios_(ios)
|
||||||
|
, strand_(ios_)
|
||||||
|
, stream_(ios_)
|
||||||
|
, ws_(stream_)
|
||||||
|
, callback_(callback)
|
||||||
|
, j_{j}
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
boost::asio::ip::tcp::endpoint const ep{ip, port};
|
||||||
|
stream_.connect(ep);
|
||||||
|
ws_.set_option(boost::beast::websocket::stream_base::decorator(
|
||||||
|
[&](boost::beast::websocket::request_type& req) {
|
||||||
|
for (auto const& h : headers)
|
||||||
|
req.set(h.first, h.second);
|
||||||
|
}));
|
||||||
|
ws_.handshake(
|
||||||
|
ep.address().to_string() + ":" + std::to_string(ep.port()), "/");
|
||||||
|
ws_.async_read(
|
||||||
|
rb_,
|
||||||
|
strand_.wrap(std::bind(
|
||||||
|
&WebsocketClient::onReadMsg, this, std::placeholders::_1)));
|
||||||
|
}
|
||||||
|
catch (std::exception&)
|
||||||
|
{
|
||||||
|
cleanup();
|
||||||
|
Rethrow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WebsocketClient::~WebsocketClient()
|
||||||
|
{
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t
|
||||||
|
WebsocketClient::send(std::string const& cmd, Json::Value params)
|
||||||
|
{
|
||||||
|
params[jss::method] = cmd;
|
||||||
|
params[jss::jsonrpc] = "2.0";
|
||||||
|
params[jss::ripplerpc] = "2.0";
|
||||||
|
|
||||||
|
auto const id = nextId_++;
|
||||||
|
params[jss::id] = id;
|
||||||
|
auto const s = to_string(params);
|
||||||
|
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
ws_.write_some(true, boost::asio::buffer(s));
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
WebsocketClient::onReadMsg(error_code const& ec)
|
||||||
|
{
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
JLOGV(j_.trace(), "WebsocketClient::onReadMsg error", jv("ec", ec));
|
||||||
|
if (ec == boost::beast::websocket::error::closed)
|
||||||
|
peerClosed_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value jval;
|
||||||
|
Json::Reader jr;
|
||||||
|
jr.parse(buffer_string(rb_.data()), jval);
|
||||||
|
rb_.consume(rb_.size());
|
||||||
|
JLOGV(j_.trace(), "WebsocketClient::onReadMsg", jv("msg", jval));
|
||||||
|
callback_(jval);
|
||||||
|
|
||||||
|
std::lock_guard l{m_};
|
||||||
|
ws_.async_read(
|
||||||
|
rb_,
|
||||||
|
strand_.wrap(std::bind(
|
||||||
|
&WebsocketClient::onReadMsg, this, std::placeholders::_1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the read op terminates
|
||||||
|
void
|
||||||
|
WebsocketClient::onReadDone()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
105
src/ripple/app/sidechain/impl/WebsocketClient.h
Normal file
105
src/ripple/app/sidechain/impl/WebsocketClient.h
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#ifndef RIPPLE_SIDECHAIN_IO_WEBSOCKET_CLIENT_H_INCLUDED
|
||||||
|
#define RIPPLE_SIDECHAIN_IO_WEBSOCKET_CLIENT_H_INCLUDED
|
||||||
|
|
||||||
|
#include <ripple/basics/ThreadSaftyAnalysis.h>
|
||||||
|
#include <ripple/core/Config.h>
|
||||||
|
|
||||||
|
#include <boost/asio/io_service.hpp>
|
||||||
|
#include <boost/asio/ip/tcp.hpp>
|
||||||
|
#include <boost/asio/strand.hpp>
|
||||||
|
#include <boost/beast/core/multi_buffer.hpp>
|
||||||
|
#include <boost/beast/websocket/stream.hpp>
|
||||||
|
#include <boost/optional.hpp>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace sidechain {
|
||||||
|
|
||||||
|
class WebsocketClient
|
||||||
|
{
|
||||||
|
using error_code = boost::system::error_code;
|
||||||
|
|
||||||
|
template <class ConstBuffers>
|
||||||
|
static std::string
|
||||||
|
buffer_string(ConstBuffers const& b);
|
||||||
|
|
||||||
|
// mutex for ws_
|
||||||
|
std::mutex m_;
|
||||||
|
|
||||||
|
// mutex for shutdown
|
||||||
|
std::mutex shutdownM_;
|
||||||
|
bool isShutdown_ = false;
|
||||||
|
std::condition_variable shutdownCv_;
|
||||||
|
|
||||||
|
boost::asio::io_service& ios_;
|
||||||
|
boost::asio::io_service::strand strand_;
|
||||||
|
boost::asio::ip::tcp::socket stream_;
|
||||||
|
boost::beast::websocket::stream<boost::asio::ip::tcp::socket&> GUARDED_BY(
|
||||||
|
m_) ws_;
|
||||||
|
boost::beast::multi_buffer rb_;
|
||||||
|
|
||||||
|
std::atomic<bool> peerClosed_{false};
|
||||||
|
|
||||||
|
std::function<void(Json::Value const&)> callback_;
|
||||||
|
std::atomic<std::uint32_t> nextId_{0};
|
||||||
|
|
||||||
|
beast::Journal j_;
|
||||||
|
|
||||||
|
void
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
public:
|
||||||
|
// callback will be called from a io_service thread
|
||||||
|
WebsocketClient(
|
||||||
|
std::function<void(Json::Value const&)> callback,
|
||||||
|
boost::asio::io_service& ios,
|
||||||
|
boost::asio::ip::address const& ip,
|
||||||
|
std::uint16_t port,
|
||||||
|
std::unordered_map<std::string, std::string> const& headers,
|
||||||
|
beast::Journal j);
|
||||||
|
|
||||||
|
~WebsocketClient();
|
||||||
|
|
||||||
|
// Returns command id that will be returned in the response
|
||||||
|
std::uint32_t
|
||||||
|
send(std::string const& cmd, Json::Value params) EXCLUDES(m_);
|
||||||
|
|
||||||
|
void
|
||||||
|
shutdown() EXCLUDES(shutdownM_);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void
|
||||||
|
onReadMsg(error_code const& ec) EXCLUDES(m_);
|
||||||
|
|
||||||
|
// Called when the read op terminates
|
||||||
|
void
|
||||||
|
onReadDone();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sidechain
|
||||||
|
} // namespace ripple
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -27,8 +27,15 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <string_view>
|
||||||
|
#include <type_traits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
namespace Json {
|
||||||
|
class Value;
|
||||||
|
class Compact;
|
||||||
|
} // namespace Json
|
||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
|
|
||||||
// DEPRECATED use beast::severities::Severity instead
|
// DEPRECATED use beast::severities::Severity instead
|
||||||
@@ -256,6 +263,92 @@ private:
|
|||||||
x
|
x
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// terse "std::tie"
|
||||||
|
template <class T1, class T2>
|
||||||
|
std::tuple<T1 const&, T2 const&>
|
||||||
|
jv(T1 const& t1, T2 const& t2)
|
||||||
|
{
|
||||||
|
return std::tie(t1, t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Stream>
|
||||||
|
void
|
||||||
|
jlogv_fields(Stream&& stream, char const* sep)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Stream, class T1, class T2, class... Ts>
|
||||||
|
void
|
||||||
|
jlogv_fields(
|
||||||
|
Stream&& stream,
|
||||||
|
char const* sep,
|
||||||
|
std::tuple<T1 const&, T2 const&> const& nameValue,
|
||||||
|
Ts&&... nameValues)
|
||||||
|
{
|
||||||
|
bool const withQuotes = [&] {
|
||||||
|
if constexpr (std::is_arithmetic_v<T2>)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if constexpr (std::is_same_v<std::decay_t<T2>, Json::Value>)
|
||||||
|
{
|
||||||
|
auto const& v = std::get<1>(nameValue);
|
||||||
|
return !v.isObject() && !v.isNumeric() && !v.isBool();
|
||||||
|
}
|
||||||
|
if constexpr (std::is_same_v<std::decay_t<T2>, Json::Compact>)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}();
|
||||||
|
|
||||||
|
stream << sep << '"' << std::get<0>(nameValue) << "\": ";
|
||||||
|
if constexpr (std::is_same_v<std::decay_t<T2>, Json::Value>)
|
||||||
|
{
|
||||||
|
stream << string_for_log(std::get<1>(nameValue));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (withQuotes)
|
||||||
|
{
|
||||||
|
// print the value with quotes
|
||||||
|
stream << '"' << std::get<1>(nameValue) << '"';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stream << std::get<1>(nameValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jlogv_fields(
|
||||||
|
std::forward<Stream>(stream), ", ", std::forward<Ts>(nameValues)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Stream, class... Ts>
|
||||||
|
void
|
||||||
|
jlogv(
|
||||||
|
Stream&& stream,
|
||||||
|
std::size_t lineNo,
|
||||||
|
std::string_view const& msg,
|
||||||
|
Ts&&... nameValues)
|
||||||
|
{
|
||||||
|
beast::Journal::ScopedStream s{std::forward<Stream>(stream), msg};
|
||||||
|
s << " {";
|
||||||
|
jlogv_fields(s, "", std::forward<Ts>(nameValues)..., jv("jlogId", lineNo));
|
||||||
|
s << '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wraps a Journal::Stream to skip evaluation of
|
||||||
|
// expensive argument lists if the stream is not active.
|
||||||
|
#ifndef JLOGV
|
||||||
|
#define JLOGV(x, msg, ...) \
|
||||||
|
if (!x) \
|
||||||
|
{ \
|
||||||
|
} \
|
||||||
|
else \
|
||||||
|
jlogv(x, __LINE__, msg, __VA_ARGS__)
|
||||||
|
#endif
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Debug logging:
|
// Debug logging:
|
||||||
|
|
||||||
|
|||||||
82
src/ripple/basics/ThreadSaftyAnalysis.h
Normal file
82
src/ripple/basics/ThreadSaftyAnalysis.h
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#ifndef RIPPLE_BASICS_THREAD_SAFTY_ANALYSIS_H_INCLUDED
|
||||||
|
#define RIPPLE_BASICS_THREAD_SAFTY_ANALYSIS_H_INCLUDED
|
||||||
|
|
||||||
|
#ifdef RIPPLE_ENABLE_THREAD_SAFETY_ANNOTATIONS
|
||||||
|
#define THREAD_ANNOTATION_ATTRIBUTE__(x) __attribute__((x))
|
||||||
|
#else
|
||||||
|
#define THREAD_ANNOTATION_ATTRIBUTE__(x) // no-op
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define CAPABILITY(x) THREAD_ANNOTATION_ATTRIBUTE__(capability(x))
|
||||||
|
|
||||||
|
#define SCOPED_CAPABILITY THREAD_ANNOTATION_ATTRIBUTE__(scoped_lockable)
|
||||||
|
|
||||||
|
#define GUARDED_BY(x) THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x))
|
||||||
|
|
||||||
|
#define PT_GUARDED_BY(x) THREAD_ANNOTATION_ATTRIBUTE__(pt_guarded_by(x))
|
||||||
|
|
||||||
|
#define ACQUIRED_BEFORE(...) \
|
||||||
|
THREAD_ANNOTATION_ATTRIBUTE__(acquired_before(__VA_ARGS__))
|
||||||
|
|
||||||
|
#define ACQUIRED_AFTER(...) \
|
||||||
|
THREAD_ANNOTATION_ATTRIBUTE__(acquired_after(__VA_ARGS__))
|
||||||
|
|
||||||
|
#define REQUIRES(...) \
|
||||||
|
THREAD_ANNOTATION_ATTRIBUTE__(requires_capability(__VA_ARGS__))
|
||||||
|
|
||||||
|
#define REQUIRES_SHARED(...) \
|
||||||
|
THREAD_ANNOTATION_ATTRIBUTE__(requires_shared_capability(__VA_ARGS__))
|
||||||
|
|
||||||
|
#define ACQUIRE(...) \
|
||||||
|
THREAD_ANNOTATION_ATTRIBUTE__(acquire_capability(__VA_ARGS__))
|
||||||
|
|
||||||
|
#define ACQUIRE_SHARED(...) \
|
||||||
|
THREAD_ANNOTATION_ATTRIBUTE__(acquire_shared_capability(__VA_ARGS__))
|
||||||
|
|
||||||
|
#define RELEASE(...) \
|
||||||
|
THREAD_ANNOTATION_ATTRIBUTE__(release_capability(__VA_ARGS__))
|
||||||
|
|
||||||
|
#define RELEASE_SHARED(...) \
|
||||||
|
THREAD_ANNOTATION_ATTRIBUTE__(release_shared_capability(__VA_ARGS__))
|
||||||
|
|
||||||
|
#define RELEASE_GENERIC(...) \
|
||||||
|
THREAD_ANNOTATION_ATTRIBUTE__(release_generic_capability(__VA_ARGS__))
|
||||||
|
|
||||||
|
#define TRY_ACQUIRE(...) \
|
||||||
|
THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_capability(__VA_ARGS__))
|
||||||
|
|
||||||
|
#define TRY_ACQUIRE_SHARED(...) \
|
||||||
|
THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_shared_capability(__VA_ARGS__))
|
||||||
|
|
||||||
|
#define EXCLUDES(...) THREAD_ANNOTATION_ATTRIBUTE__(locks_excluded(__VA_ARGS__))
|
||||||
|
|
||||||
|
#define ASSERT_CAPABILITY(x) THREAD_ANNOTATION_ATTRIBUTE__(assert_capability(x))
|
||||||
|
|
||||||
|
#define ASSERT_SHARED_CAPABILITY(x) \
|
||||||
|
THREAD_ANNOTATION_ATTRIBUTE__(assert_shared_capability(x))
|
||||||
|
|
||||||
|
#define RETURN_CAPABILITY(x) THREAD_ANNOTATION_ATTRIBUTE__(lock_returned(x))
|
||||||
|
|
||||||
|
#define NO_THREAD_SAFETY_ANALYSIS \
|
||||||
|
THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis)
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -134,7 +134,6 @@ public:
|
|||||||
|
|
||||||
class Stream;
|
class Stream;
|
||||||
|
|
||||||
private:
|
|
||||||
/* Scoped ostream-based container for writing messages to a Journal. */
|
/* Scoped ostream-based container for writing messages to a Journal. */
|
||||||
class ScopedStream
|
class ScopedStream
|
||||||
{
|
{
|
||||||
@@ -189,7 +188,6 @@ private:
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
public:
|
|
||||||
/** Provide a light-weight way to check active() before string formatting */
|
/** Provide a light-weight way to check active() before string formatting */
|
||||||
class Stream
|
class Stream
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -60,20 +60,21 @@ enum JobType {
|
|||||||
jtREPLAY_TASK, // A Ledger replay task/subtask
|
jtREPLAY_TASK, // A Ledger replay task/subtask
|
||||||
jtLEDGER_DATA, // Received data for a ledger we're acquiring
|
jtLEDGER_DATA, // Received data for a ledger we're acquiring
|
||||||
jtTRANSACTION, // A transaction received from the network
|
jtTRANSACTION, // A transaction received from the network
|
||||||
jtMISSING_TXN, // Request missing transactions
|
jtFEDERATORSIGNATURE, // A signature from a sidechain federator
|
||||||
jtREQUESTED_TXN, // Reply with requested transactions
|
jtMISSING_TXN, // Request missing transactions
|
||||||
jtBATCH, // Apply batched transactions
|
jtREQUESTED_TXN, // Reply with requested transactions
|
||||||
jtADVANCE, // Advance validated/acquired ledgers
|
jtBATCH, // Apply batched transactions
|
||||||
jtPUBLEDGER, // Publish a fully-accepted ledger
|
jtADVANCE, // Advance validated/acquired ledgers
|
||||||
jtTXN_DATA, // Fetch a proposed set
|
jtPUBLEDGER, // Publish a fully-accepted ledger
|
||||||
jtWAL, // Write-ahead logging
|
jtTXN_DATA, // Fetch a proposed set
|
||||||
jtVALIDATION_t, // A validation from a trusted source
|
jtWAL, // Write-ahead logging
|
||||||
jtWRITE, // Write out hashed objects
|
jtVALIDATION_t, // A validation from a trusted source
|
||||||
jtACCEPT, // Accept a consensus ledger
|
jtWRITE, // Write out hashed objects
|
||||||
jtPROPOSAL_t, // A proposal from a trusted source
|
jtACCEPT, // Accept a consensus ledger
|
||||||
jtNETOP_CLUSTER, // NetworkOPs cluster peer report
|
jtPROPOSAL_t, // A proposal from a trusted source
|
||||||
jtNETOP_TIMER, // NetworkOPs net timer processing
|
jtNETOP_CLUSTER, // NetworkOPs cluster peer report
|
||||||
jtADMIN, // An administrative operation
|
jtNETOP_TIMER, // NetworkOPs net timer processing
|
||||||
|
jtADMIN, // An administrative operation
|
||||||
|
|
||||||
// Special job types which are not dispatched by the job pool
|
// Special job types which are not dispatched by the job pool
|
||||||
jtPEER,
|
jtPEER,
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ private:
|
|||||||
add(jtUPDATE_PF, "updatePaths", 1, 0ms, 0ms);
|
add(jtUPDATE_PF, "updatePaths", 1, 0ms, 0ms);
|
||||||
add(jtTRANSACTION, "transaction", maxLimit, 250ms, 1000ms);
|
add(jtTRANSACTION, "transaction", maxLimit, 250ms, 1000ms);
|
||||||
add(jtBATCH, "batch", maxLimit, 250ms, 1000ms);
|
add(jtBATCH, "batch", maxLimit, 250ms, 1000ms);
|
||||||
|
// TODO chose ave latency and peak latency numbers
|
||||||
|
add(jtFEDERATORSIGNATURE, "federatorSignature", maxLimit, 250ms, 1000ms);
|
||||||
add(jtADVANCE, "advanceLedger", maxLimit, 0ms, 0ms);
|
add(jtADVANCE, "advanceLedger", maxLimit, 0ms, 0ms);
|
||||||
add(jtPUBLEDGER, "publishNewLedger", maxLimit, 3000ms, 4500ms);
|
add(jtPUBLEDGER, "publishNewLedger", maxLimit, 3000ms, 4500ms);
|
||||||
add(jtTXN_DATA, "fetchTxnData", 5, 0ms, 0ms);
|
add(jtTXN_DATA, "fetchTxnData", 5, 0ms, 0ms);
|
||||||
|
|||||||
@@ -342,6 +342,13 @@ public:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline std::string
|
||||||
|
string_for_log(Json::Value const& v)
|
||||||
|
{
|
||||||
|
FastWriter w;
|
||||||
|
return w.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Json
|
} // namespace Json
|
||||||
|
|
||||||
#endif // JSON_WRITER_H_INCLUDED
|
#endif // JSON_WRITER_H_INCLUDED
|
||||||
|
|||||||
@@ -1250,6 +1250,7 @@ public:
|
|||||||
{"deposit_authorized", &RPCParser::parseDepositAuthorized, 2, 3},
|
{"deposit_authorized", &RPCParser::parseDepositAuthorized, 2, 3},
|
||||||
{"download_shard", &RPCParser::parseDownloadShard, 2, -1},
|
{"download_shard", &RPCParser::parseDownloadShard, 2, -1},
|
||||||
{"feature", &RPCParser::parseFeature, 0, 2},
|
{"feature", &RPCParser::parseFeature, 0, 2},
|
||||||
|
{"federator_info", &RPCParser::parseAsIs, 0, 0},
|
||||||
{"fetch_info", &RPCParser::parseFetchInfo, 0, 1},
|
{"fetch_info", &RPCParser::parseFetchInfo, 0, 1},
|
||||||
{"gateway_balances", &RPCParser::parseGatewayBalances, 1, -1},
|
{"gateway_balances", &RPCParser::parseGatewayBalances, 1, -1},
|
||||||
{"get_counts", &RPCParser::parseGetCounts, 0, 1},
|
{"get_counts", &RPCParser::parseGetCounts, 0, 1},
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ Message::compress()
|
|||||||
case protocol::mtVALIDATORLISTCOLLECTION:
|
case protocol::mtVALIDATORLISTCOLLECTION:
|
||||||
case protocol::mtREPLAY_DELTA_RESPONSE:
|
case protocol::mtREPLAY_DELTA_RESPONSE:
|
||||||
case protocol::mtTRANSACTIONS:
|
case protocol::mtTRANSACTIONS:
|
||||||
|
case protocol::mtFederatorXChainTxnSignature:
|
||||||
return true;
|
return true;
|
||||||
case protocol::mtPING:
|
case protocol::mtPING:
|
||||||
case protocol::mtCLUSTER:
|
case protocol::mtCLUSTER:
|
||||||
@@ -102,6 +103,7 @@ Message::compress()
|
|||||||
case protocol::mtGET_PEER_SHARD_INFO_V2:
|
case protocol::mtGET_PEER_SHARD_INFO_V2:
|
||||||
case protocol::mtPEER_SHARD_INFO_V2:
|
case protocol::mtPEER_SHARD_INFO_V2:
|
||||||
case protocol::mtHAVE_TRANSACTIONS:
|
case protocol::mtHAVE_TRANSACTIONS:
|
||||||
|
case protocol::mtFederatorAccountCtrlSignature:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#include <ripple/app/misc/NetworkOPs.h>
|
#include <ripple/app/misc/NetworkOPs.h>
|
||||||
#include <ripple/app/misc/Transaction.h>
|
#include <ripple/app/misc/Transaction.h>
|
||||||
#include <ripple/app/misc/ValidatorList.h>
|
#include <ripple/app/misc/ValidatorList.h>
|
||||||
|
#include <ripple/app/sidechain/Federator.h>
|
||||||
#include <ripple/app/tx/apply.h>
|
#include <ripple/app/tx/apply.h>
|
||||||
#include <ripple/basics/UptimeClock.h>
|
#include <ripple/basics/UptimeClock.h>
|
||||||
#include <ripple/basics/base64.h>
|
#include <ripple/basics/base64.h>
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
#include <ripple/overlay/impl/PeerImp.h>
|
#include <ripple/overlay/impl/PeerImp.h>
|
||||||
#include <ripple/overlay/impl/Tuning.h>
|
#include <ripple/overlay/impl/Tuning.h>
|
||||||
#include <ripple/overlay/predicates.h>
|
#include <ripple/overlay/predicates.h>
|
||||||
|
#include <ripple/protocol/SField.h>
|
||||||
#include <ripple/protocol/digest.h>
|
#include <ripple/protocol/digest.h>
|
||||||
|
|
||||||
#include <boost/algorithm/string.hpp>
|
#include <boost/algorithm/string.hpp>
|
||||||
@@ -2928,6 +2930,339 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMSquelch> const& m)
|
|||||||
<< "onMessage: TMSquelch " << slice << " " << id() << " " << duration;
|
<< "onMessage: TMSquelch " << slice << " " << id() << " " << duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PeerImp::onMessage(
|
||||||
|
std::shared_ptr<protocol::TMFederatorXChainTxnSignature> const& m)
|
||||||
|
{
|
||||||
|
std::shared_ptr<sidechain::Federator> federator =
|
||||||
|
app_.getSidechainFederator();
|
||||||
|
|
||||||
|
auto sidechainJ = app_.journal("SidechainFederator");
|
||||||
|
|
||||||
|
auto badData = [&](std::string msg) {
|
||||||
|
fee_ = Resource::feeBadData;
|
||||||
|
JLOG(p_journal_.warn()) << msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto getTxnType = [](::protocol::TMFederatorTxnType tt)
|
||||||
|
-> std::optional<sidechain::Federator::TxnType> {
|
||||||
|
switch (tt)
|
||||||
|
{
|
||||||
|
case ::protocol::TMFederatorTxnType::ftxnt_XCHAIN:
|
||||||
|
return sidechain::Federator::TxnType::xChain;
|
||||||
|
case ::protocol::TMFederatorTxnType::ftxnt_REFUND:
|
||||||
|
return sidechain::Federator::TxnType::refund;
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto getChainType = [](::protocol::TMFederatorChainType ct)
|
||||||
|
-> std::optional<sidechain::Federator::ChainType> {
|
||||||
|
switch (ct)
|
||||||
|
{
|
||||||
|
case ::protocol::TMFederatorChainType::fct_SIDE:
|
||||||
|
return sidechain::Federator::sideChain;
|
||||||
|
case ::protocol::TMFederatorChainType::fct_MAIN:
|
||||||
|
return sidechain::Federator::mainChain;
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto getPublicKey =
|
||||||
|
[](std::string const& data) -> std::optional<PublicKey> {
|
||||||
|
if (data.empty())
|
||||||
|
return {};
|
||||||
|
auto const s = makeSlice(data);
|
||||||
|
if (!publicKeyType(s))
|
||||||
|
return {};
|
||||||
|
return PublicKey(s);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto getHash = [](std::string const& data) -> std::optional<uint256> {
|
||||||
|
if (data.size() != 32)
|
||||||
|
return {};
|
||||||
|
return uint256(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto getAccountId =
|
||||||
|
[](std::string const& data) -> std::optional<AccountID> {
|
||||||
|
if (data.size() != 20)
|
||||||
|
return {};
|
||||||
|
return AccountID(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto const signingPK = getPublicKey(m->signingpk());
|
||||||
|
if (!signingPK)
|
||||||
|
{
|
||||||
|
return badData("Invalid federator key");
|
||||||
|
}
|
||||||
|
auto const txnType = getTxnType(m->txntype());
|
||||||
|
if (!txnType)
|
||||||
|
{
|
||||||
|
return badData("Invalid txn type");
|
||||||
|
}
|
||||||
|
auto const dstChain = getChainType(m->dstchain());
|
||||||
|
if (!dstChain)
|
||||||
|
{
|
||||||
|
return badData("Invalid dst chain");
|
||||||
|
}
|
||||||
|
auto const srcChainTxnHash = getHash(m->srcchaintxnhash());
|
||||||
|
if (!srcChainTxnHash)
|
||||||
|
{
|
||||||
|
return badData("Invalid src chain txn hash");
|
||||||
|
}
|
||||||
|
auto const dstChainTxnHash = getHash(m->dstchaintxnhash());
|
||||||
|
if (*txnType == sidechain::Federator::TxnType::refund && !dstChainTxnHash)
|
||||||
|
{
|
||||||
|
return badData("Invalid dst chain txn hash for refund");
|
||||||
|
}
|
||||||
|
if (*txnType == sidechain::Federator::TxnType::xChain && dstChainTxnHash)
|
||||||
|
{
|
||||||
|
return badData("Invalid dst chain txn hash for xchain");
|
||||||
|
}
|
||||||
|
auto const srcChainSrcAccount = getAccountId(m->srcchainsrcaccount());
|
||||||
|
if (!srcChainSrcAccount)
|
||||||
|
{
|
||||||
|
return badData("Invalid src chain src account");
|
||||||
|
}
|
||||||
|
auto const dstChainSrcAccount = getAccountId(m->dstchainsrcaccount());
|
||||||
|
if (!dstChainSrcAccount)
|
||||||
|
{
|
||||||
|
return badData("Invalid dst chain src account");
|
||||||
|
}
|
||||||
|
auto const dstChainDstAccount = getAccountId(m->dstchaindstaccount());
|
||||||
|
if (!dstChainDstAccount)
|
||||||
|
{
|
||||||
|
return badData("Invalid dst account");
|
||||||
|
}
|
||||||
|
auto const seq = m->seq();
|
||||||
|
auto const amt = [&m]() -> std::optional<STAmount> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SerialIter iter{m->amount().data(), m->amount().size()};
|
||||||
|
STAmount amt{iter, sfGeneric};
|
||||||
|
if (!iter.empty() || amt.signum() <= 0)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
if (amt.native() && amt.xrp() > INITIAL_XRP)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return amt;
|
||||||
|
}
|
||||||
|
catch (std::exception const&)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (!amt)
|
||||||
|
{
|
||||||
|
return badData("Invalid amount");
|
||||||
|
}
|
||||||
|
|
||||||
|
Buffer sig{m->signature().data(), m->signature().size()};
|
||||||
|
|
||||||
|
JLOGV(
|
||||||
|
sidechainJ.trace(),
|
||||||
|
"Received signature from peer",
|
||||||
|
jv("id", id()),
|
||||||
|
jv("sig", strHex(sig.data(), sig.data() + sig.size())));
|
||||||
|
|
||||||
|
if (federator && federator->alreadySent(*dstChain, seq))
|
||||||
|
{
|
||||||
|
// already sent this transaction, no need to forward signature
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 const suppression = sidechain::crossChainTxnSignatureId(
|
||||||
|
*signingPK,
|
||||||
|
*srcChainTxnHash,
|
||||||
|
dstChainTxnHash,
|
||||||
|
*amt,
|
||||||
|
*dstChainSrcAccount,
|
||||||
|
*dstChainDstAccount,
|
||||||
|
seq,
|
||||||
|
sig);
|
||||||
|
app_.getHashRouter().addSuppressionPeer(suppression, id_);
|
||||||
|
|
||||||
|
app_.getJobQueue().addJob(
|
||||||
|
jtFEDERATORSIGNATURE,
|
||||||
|
"federator signature",
|
||||||
|
[self = shared_from_this(),
|
||||||
|
federator = std::move(federator),
|
||||||
|
suppression,
|
||||||
|
txnType,
|
||||||
|
dstChain,
|
||||||
|
signingPK,
|
||||||
|
srcChainTxnHash,
|
||||||
|
dstChainTxnHash,
|
||||||
|
amt,
|
||||||
|
srcChainSrcAccount,
|
||||||
|
dstChainDstAccount,
|
||||||
|
seq,
|
||||||
|
m,
|
||||||
|
j = sidechainJ,
|
||||||
|
sig = std::move(sig)]() mutable {
|
||||||
|
auto& hashRouter = self->app_.getHashRouter();
|
||||||
|
if (auto const toSkip = hashRouter.shouldRelay(suppression))
|
||||||
|
{
|
||||||
|
auto const toSend = std::make_shared<Message>(
|
||||||
|
*m, protocol::mtFederatorXChainTxnSignature);
|
||||||
|
self->overlay_.foreach([&](std::shared_ptr<Peer> const& p) {
|
||||||
|
hashRouter.addSuppressionPeer(suppression, p->id());
|
||||||
|
if (toSkip->count(p->id()))
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j.trace(),
|
||||||
|
"Not forwarding signature to peer",
|
||||||
|
jv("id", p->id()),
|
||||||
|
jv("suppression", suppression));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JLOGV(
|
||||||
|
j.trace(),
|
||||||
|
"Forwarding signature to peer",
|
||||||
|
jv("id", p->id()),
|
||||||
|
jv("suppression", suppression));
|
||||||
|
p->send(toSend);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (federator)
|
||||||
|
{
|
||||||
|
// Signature is checked in `addPendingTxnSig`
|
||||||
|
federator->addPendingTxnSig(
|
||||||
|
*txnType,
|
||||||
|
*dstChain,
|
||||||
|
*signingPK,
|
||||||
|
*srcChainTxnHash,
|
||||||
|
dstChainTxnHash,
|
||||||
|
*amt,
|
||||||
|
*srcChainSrcAccount,
|
||||||
|
*dstChainDstAccount,
|
||||||
|
seq,
|
||||||
|
std::move(sig));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PeerImp::onMessage(
|
||||||
|
std::shared_ptr<protocol::TMFederatorAccountCtrlSignature> const& m)
|
||||||
|
{
|
||||||
|
std::shared_ptr<sidechain::Federator> federator =
|
||||||
|
app_.getSidechainFederator();
|
||||||
|
|
||||||
|
auto sidechainJ = app_.journal("SidechainFederator");
|
||||||
|
|
||||||
|
auto badData = [&](std::string msg) {
|
||||||
|
fee_ = Resource::feeBadData;
|
||||||
|
JLOG(p_journal_.warn()) << msg;
|
||||||
|
};
|
||||||
|
auto getChainType = [](::protocol::TMFederatorChainType ct)
|
||||||
|
-> std::optional<sidechain::Federator::ChainType> {
|
||||||
|
switch (ct)
|
||||||
|
{
|
||||||
|
case ::protocol::TMFederatorChainType::fct_SIDE:
|
||||||
|
return sidechain::Federator::sideChain;
|
||||||
|
case ::protocol::TMFederatorChainType::fct_MAIN:
|
||||||
|
return sidechain::Federator::mainChain;
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
auto const dstChain = getChainType(m->chain());
|
||||||
|
if (!dstChain)
|
||||||
|
{
|
||||||
|
return badData("Invalid dst chain");
|
||||||
|
}
|
||||||
|
auto getPublicKey =
|
||||||
|
[](std::string const& data) -> std::optional<PublicKey> {
|
||||||
|
if (data.empty())
|
||||||
|
return {};
|
||||||
|
auto const s = makeSlice(data);
|
||||||
|
if (!publicKeyType(s))
|
||||||
|
return {};
|
||||||
|
return PublicKey(s);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto getHash = [](std::string const& data) -> std::optional<uint256> {
|
||||||
|
if (data.size() != 32)
|
||||||
|
return {};
|
||||||
|
return uint256(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto const pk = getPublicKey(m->publickey());
|
||||||
|
if (!pk)
|
||||||
|
{
|
||||||
|
return badData("Invalid federator key");
|
||||||
|
}
|
||||||
|
auto const mId = getHash(m->messageid());
|
||||||
|
if (!mId)
|
||||||
|
{
|
||||||
|
return badData("Invalid txn hash");
|
||||||
|
}
|
||||||
|
|
||||||
|
Buffer sig{m->signature().data(), m->signature().size()};
|
||||||
|
|
||||||
|
JLOGV(
|
||||||
|
sidechainJ.trace(),
|
||||||
|
"Received signature from peer",
|
||||||
|
jv("id", id()),
|
||||||
|
jv("sig", strHex(sig.data(), sig.data() + sig.size())));
|
||||||
|
|
||||||
|
uint256 const suppression = sidechain::computeMessageSuppression(*mId, sig);
|
||||||
|
app_.getHashRouter().addSuppressionPeer(suppression, id_);
|
||||||
|
|
||||||
|
app_.getJobQueue().addJob(
|
||||||
|
jtFEDERATORSIGNATURE,
|
||||||
|
"federator signature",
|
||||||
|
[self = shared_from_this(),
|
||||||
|
federator = std::move(federator),
|
||||||
|
suppression,
|
||||||
|
pk,
|
||||||
|
mId,
|
||||||
|
m,
|
||||||
|
j = sidechainJ,
|
||||||
|
chain = *dstChain,
|
||||||
|
sig = std::move(sig)]() mutable {
|
||||||
|
auto& hashRouter = self->app_.getHashRouter();
|
||||||
|
if (auto const toSkip = hashRouter.shouldRelay(suppression))
|
||||||
|
{
|
||||||
|
auto const toSend = std::make_shared<Message>(
|
||||||
|
*m, protocol::mtFederatorAccountCtrlSignature);
|
||||||
|
self->overlay_.foreach([&](std::shared_ptr<Peer> const& p) {
|
||||||
|
hashRouter.addSuppressionPeer(suppression, p->id());
|
||||||
|
if (toSkip->count(p->id()))
|
||||||
|
{
|
||||||
|
JLOGV(
|
||||||
|
j.trace(),
|
||||||
|
"Not forwarding signature to peer",
|
||||||
|
jv("id", p->id()),
|
||||||
|
jv("suppression", suppression));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JLOGV(
|
||||||
|
j.trace(),
|
||||||
|
"Forwarding signature to peer",
|
||||||
|
jv("id", p->id()),
|
||||||
|
jv("suppression", suppression));
|
||||||
|
p->send(toSend);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (federator)
|
||||||
|
{
|
||||||
|
// Signature is checked in `addPendingTxnSig`
|
||||||
|
federator->addPendingTxnSig(chain, *pk, *mId, std::move(sig));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|||||||
@@ -584,6 +584,12 @@ public:
|
|||||||
onMessage(std::shared_ptr<protocol::TMReplayDeltaRequest> const& m);
|
onMessage(std::shared_ptr<protocol::TMReplayDeltaRequest> const& m);
|
||||||
void
|
void
|
||||||
onMessage(std::shared_ptr<protocol::TMReplayDeltaResponse> const& m);
|
onMessage(std::shared_ptr<protocol::TMReplayDeltaResponse> const& m);
|
||||||
|
void
|
||||||
|
onMessage(
|
||||||
|
std::shared_ptr<protocol::TMFederatorXChainTxnSignature> const& m);
|
||||||
|
void
|
||||||
|
onMessage(
|
||||||
|
std::shared_ptr<protocol::TMFederatorAccountCtrlSignature> const& m);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ protocolMessageName(int type)
|
|||||||
return "get_peer_shard_info_v2";
|
return "get_peer_shard_info_v2";
|
||||||
case protocol::mtPEER_SHARD_INFO_V2:
|
case protocol::mtPEER_SHARD_INFO_V2:
|
||||||
return "peer_shard_info_v2";
|
return "peer_shard_info_v2";
|
||||||
|
case protocol::mtFederatorXChainTxnSignature:
|
||||||
|
return "federator_xchain_txn_signature";
|
||||||
|
case protocol::mtFederatorAccountCtrlSignature:
|
||||||
|
return "federator_account_ctrl_signature";
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -493,6 +497,14 @@ invokeProtocolMessage(
|
|||||||
success = detail::invoke<protocol::TMPeerShardInfoV2>(
|
success = detail::invoke<protocol::TMPeerShardInfoV2>(
|
||||||
*header, buffers, handler);
|
*header, buffers, handler);
|
||||||
break;
|
break;
|
||||||
|
case protocol::mtFederatorXChainTxnSignature:
|
||||||
|
success = detail::invoke<protocol::TMFederatorXChainTxnSignature>(
|
||||||
|
*header, buffers, handler);
|
||||||
|
break;
|
||||||
|
case protocol::mtFederatorAccountCtrlSignature:
|
||||||
|
success = detail::invoke<protocol::TMFederatorAccountCtrlSignature>(
|
||||||
|
*header, buffers, handler);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
handler.onMessageUnknown(header->message_type);
|
handler.onMessageUnknown(header->message_type);
|
||||||
success = true;
|
success = true;
|
||||||
|
|||||||
@@ -163,6 +163,9 @@ TrafficCount::categorize(
|
|||||||
if (type == protocol::mtTRANSACTIONS)
|
if (type == protocol::mtTRANSACTIONS)
|
||||||
return TrafficCount::category::requested_transactions;
|
return TrafficCount::category::requested_transactions;
|
||||||
|
|
||||||
|
if (type == protocol::mtFederatorXChainTxnSignature)
|
||||||
|
return TrafficCount::category::federator_xchain_txn_signature;
|
||||||
|
|
||||||
return TrafficCount::category::unknown;
|
return TrafficCount::category::unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ public:
|
|||||||
// TMTransactions
|
// TMTransactions
|
||||||
requested_transactions,
|
requested_transactions,
|
||||||
|
|
||||||
|
federator_xchain_txn_signature,
|
||||||
|
|
||||||
unknown // must be last
|
unknown // must be last
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -243,12 +245,13 @@ protected:
|
|||||||
{"getobject_share"}, // category::share_hash
|
{"getobject_share"}, // category::share_hash
|
||||||
{"getobject_get"}, // category::get_hash
|
{"getobject_get"}, // category::get_hash
|
||||||
{"proof_path_request"}, // category::proof_path_request
|
{"proof_path_request"}, // category::proof_path_request
|
||||||
{"proof_path_response"}, // category::proof_path_response
|
{"proof_path_response"}, // category::proof_path_response
|
||||||
{"replay_delta_request"}, // category::replay_delta_request
|
{"replay_delta_request"}, // category::replay_delta_request
|
||||||
{"replay_delta_response"}, // category::replay_delta_response
|
{"replay_delta_response"}, // category::replay_delta_response
|
||||||
{"have_transactions"}, // category::have_transactions
|
{"have_transactions"}, // category::have_transactions
|
||||||
{"requested_transactions"}, // category::transactions
|
{"requested_transactions"}, // category::transactions
|
||||||
{"unknown"} // category::unknown
|
{"federator_xchain_txn_signature"}, // category::federator_xchain_txn_signature
|
||||||
|
{"unknown"} // category::unknown
|
||||||
}};
|
}};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ enum MessageType
|
|||||||
mtPEER_SHARD_INFO_V2 = 62;
|
mtPEER_SHARD_INFO_V2 = 62;
|
||||||
mtHAVE_TRANSACTIONS = 63;
|
mtHAVE_TRANSACTIONS = 63;
|
||||||
mtTRANSACTIONS = 64;
|
mtTRANSACTIONS = 64;
|
||||||
|
mtFederatorXChainTxnSignature = 65;
|
||||||
|
mtFederatorAccountCtrlSignature = 66;
|
||||||
}
|
}
|
||||||
|
|
||||||
// token, iterations, target, challenge = issue demand for proof of work
|
// token, iterations, target, challenge = issue demand for proof of work
|
||||||
@@ -450,3 +452,42 @@ message TMHaveTransactions
|
|||||||
repeated bytes hashes = 1;
|
repeated bytes hashes = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TMFederatorChainType
|
||||||
|
{
|
||||||
|
fct_SIDE = 1;
|
||||||
|
fct_MAIN = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TMFederatorTxnType
|
||||||
|
{
|
||||||
|
ftxnt_XCHAIN = 1; // cross chain
|
||||||
|
ftxnt_REFUND = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TMFederatorXChainTxnSignature
|
||||||
|
{
|
||||||
|
required TMFederatorTxnType txnType = 1;
|
||||||
|
required TMFederatorChainType dstChain = 2;
|
||||||
|
required bytes signingPK = 3; // federator's signing public key (unencoded binary data)
|
||||||
|
// txn hash for the src chain (unencoded bigendian binary data)
|
||||||
|
// This will be origional transaction from the src account to the door account
|
||||||
|
required bytes srcChainTxnHash = 4;
|
||||||
|
// txn hash for the dst chain (unencoded bigendian binary data)
|
||||||
|
// This will be empty for XCHAIN transaction, and will the failed transaction from the
|
||||||
|
// door account to the dst account for refund txns.
|
||||||
|
optional bytes dstChainTxnHash = 5;
|
||||||
|
required bytes amount = 6; // STAmount in wire serialized format
|
||||||
|
required bytes srcChainSrcAccount = 7; // account id (unencoded bigendian binary data)
|
||||||
|
required bytes dstChainSrcAccount = 8; // account id (unencoded bigendian binary data)
|
||||||
|
required bytes dstChainDstAccount = 9; // account id (unencoded bigendian binary data)
|
||||||
|
required uint32 seq = 10; // sequence number
|
||||||
|
required bytes signature = 11; // (unencoded bigendian binary data)
|
||||||
|
}
|
||||||
|
|
||||||
|
message TMFederatorAccountCtrlSignature
|
||||||
|
{
|
||||||
|
required TMFederatorChainType chain = 1;
|
||||||
|
required bytes publicKey = 2;
|
||||||
|
required bytes messageId = 3;
|
||||||
|
required bytes signature = 4;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#ifndef RIPPLE_PROTOCOL_STTX_H_INCLUDED
|
#ifndef RIPPLE_PROTOCOL_STTX_H_INCLUDED
|
||||||
#define RIPPLE_PROTOCOL_STTX_H_INCLUDED
|
#define RIPPLE_PROTOCOL_STTX_H_INCLUDED
|
||||||
|
|
||||||
|
#include <ripple/basics/Buffer.h>
|
||||||
#include <ripple/basics/Expected.h>
|
#include <ripple/basics/Expected.h>
|
||||||
#include <ripple/protocol/PublicKey.h>
|
#include <ripple/protocol/PublicKey.h>
|
||||||
#include <ripple/protocol/STObject.h>
|
#include <ripple/protocol/STObject.h>
|
||||||
@@ -77,6 +78,20 @@ public:
|
|||||||
Blob
|
Blob
|
||||||
getSignature() const;
|
getSignature() const;
|
||||||
|
|
||||||
|
Buffer
|
||||||
|
getSignature(PublicKey const& publicKey, SecretKey const& secretKey) const;
|
||||||
|
|
||||||
|
// Get one of the multi-signatures
|
||||||
|
Buffer
|
||||||
|
getMultiSignature(
|
||||||
|
AccountID const& signingID,
|
||||||
|
PublicKey const& publicKey,
|
||||||
|
SecretKey const& secretKey) const;
|
||||||
|
|
||||||
|
// unconditionally set signature. No error checking.
|
||||||
|
void
|
||||||
|
setSignature(Buffer const& sig);
|
||||||
|
|
||||||
uint256
|
uint256
|
||||||
getSigningHash() const;
|
getSigningHash() const;
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,11 @@ parseGenericSeed(std::string const& str);
|
|||||||
std::string
|
std::string
|
||||||
seedAs1751(Seed const& seed);
|
seedAs1751(Seed const& seed);
|
||||||
|
|
||||||
|
/** ripple-lib encodes seeds used to generate an Ed25519 wallet in a
|
||||||
|
* non-standard way. */
|
||||||
|
std::optional<Seed>
|
||||||
|
parseRippleLibSeed(std::string const& s);
|
||||||
|
|
||||||
/** Format a seed as a Base58 string */
|
/** Format a seed as a Base58 string */
|
||||||
inline std::string
|
inline std::string
|
||||||
toBase58(Seed const& seed)
|
toBase58(Seed const& seed)
|
||||||
|
|||||||
@@ -160,6 +160,16 @@ getSigningData(STTx const& that)
|
|||||||
return s.getData();
|
return s.getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Blob
|
||||||
|
getMultiSigningData(STTx const& that, AccountID const& signingID)
|
||||||
|
{
|
||||||
|
Serializer s;
|
||||||
|
s.add32(HashPrefix::txMultiSign);
|
||||||
|
that.addWithoutSigningFields(s);
|
||||||
|
s.addBitString(signingID);
|
||||||
|
return s.getData();
|
||||||
|
}
|
||||||
|
|
||||||
uint256
|
uint256
|
||||||
STTx::getSigningHash() const
|
STTx::getSigningHash() const
|
||||||
{
|
{
|
||||||
@@ -179,6 +189,30 @@ STTx::getSignature() const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Buffer
|
||||||
|
STTx::getSignature(PublicKey const& publicKey, SecretKey const& secretKey) const
|
||||||
|
{
|
||||||
|
auto const data = getSigningData(*this);
|
||||||
|
return ripple::sign(publicKey, secretKey, makeSlice(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
Buffer
|
||||||
|
STTx::getMultiSignature(
|
||||||
|
AccountID const& signingID,
|
||||||
|
PublicKey const& publicKey,
|
||||||
|
SecretKey const& secretKey) const
|
||||||
|
{
|
||||||
|
auto const data = getMultiSigningData(*this, signingID);
|
||||||
|
return ripple::sign(publicKey, secretKey, makeSlice(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
STTx::setSignature(Buffer const& sig)
|
||||||
|
{
|
||||||
|
setFieldVL(sfTxnSignature, sig);
|
||||||
|
tid_ = getHash(HashPrefix::transactionID);
|
||||||
|
}
|
||||||
|
|
||||||
SeqProxy
|
SeqProxy
|
||||||
STTx::getSeqProxy() const
|
STTx::getSeqProxy() const
|
||||||
{
|
{
|
||||||
@@ -197,12 +231,7 @@ STTx::getSeqProxy() const
|
|||||||
void
|
void
|
||||||
STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
|
STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||||
{
|
{
|
||||||
auto const data = getSigningData(*this);
|
setSignature(getSignature(publicKey, secretKey));
|
||||||
|
|
||||||
auto const sig = ripple::sign(publicKey, secretKey, makeSlice(data));
|
|
||||||
|
|
||||||
setFieldVL(sfTxnSignature, sig);
|
|
||||||
tid_ = getHash(HashPrefix::transactionID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Expected<void, std::string>
|
Expected<void, std::string>
|
||||||
|
|||||||
@@ -135,4 +135,17 @@ seedAs1751(Seed const& seed)
|
|||||||
return encodedKey;
|
return encodedKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<Seed>
|
||||||
|
parseRippleLibSeed(std::string const& s)
|
||||||
|
{
|
||||||
|
auto const result = decodeBase58Token(s, TokenType::None);
|
||||||
|
|
||||||
|
if (result.size() == 18 &&
|
||||||
|
static_cast<std::uint8_t>(result[0]) == std::uint8_t(0xE1) &&
|
||||||
|
static_cast<std::uint8_t>(result[1]) == std::uint8_t(0x4B))
|
||||||
|
return Seed(makeSlice(result.substr(2)));
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ JSS(Invalid); //
|
|||||||
JSS(LastLedgerSequence); // in: TransactionSign; field
|
JSS(LastLedgerSequence); // in: TransactionSign; field
|
||||||
JSS(LedgerHashes); // ledger type.
|
JSS(LedgerHashes); // ledger type.
|
||||||
JSS(LimitAmount); // field.
|
JSS(LimitAmount); // field.
|
||||||
|
JSS(Memo); // txn common field
|
||||||
|
JSS(Memos); // txn common field
|
||||||
|
JSS(MemoType); // txn common field
|
||||||
|
JSS(MemoData); // txn common field
|
||||||
JSS(Offer); // ledger type.
|
JSS(Offer); // ledger type.
|
||||||
JSS(OfferCancel); // transaction type.
|
JSS(OfferCancel); // transaction type.
|
||||||
JSS(OfferCreate); // transaction type.
|
JSS(OfferCreate); // transaction type.
|
||||||
@@ -91,6 +95,10 @@ JSS(SetFlag); // field.
|
|||||||
JSS(SetRegularKey); // transaction type.
|
JSS(SetRegularKey); // transaction type.
|
||||||
JSS(SignerList); // ledger type.
|
JSS(SignerList); // ledger type.
|
||||||
JSS(SignerListSet); // transaction type.
|
JSS(SignerListSet); // transaction type.
|
||||||
|
JSS(SignerEntry); // transaction type.
|
||||||
|
JSS(SignerEntries); // transaction type.
|
||||||
|
JSS(SignerQuorum); // transaction type.
|
||||||
|
JSS(SignerWeight); // transaction type.
|
||||||
JSS(SigningPubKey); // field.
|
JSS(SigningPubKey); // field.
|
||||||
JSS(TakerGets); // field.
|
JSS(TakerGets); // field.
|
||||||
JSS(TakerPays); // field.
|
JSS(TakerPays); // field.
|
||||||
@@ -312,6 +320,7 @@ JSS(last_close); // out: NetworkOPs
|
|||||||
JSS(last_refresh_time); // out: ValidatorSite
|
JSS(last_refresh_time); // out: ValidatorSite
|
||||||
JSS(last_refresh_status); // out: ValidatorSite
|
JSS(last_refresh_status); // out: ValidatorSite
|
||||||
JSS(last_refresh_message); // out: ValidatorSite
|
JSS(last_refresh_message); // out: ValidatorSite
|
||||||
|
JSS(last_transaction_sent_seq); // out: federator_info
|
||||||
JSS(ledger); // in: NetworkOPs, LedgerCleaner,
|
JSS(ledger); // in: NetworkOPs, LedgerCleaner,
|
||||||
// RPCHelpers
|
// RPCHelpers
|
||||||
// out: NetworkOPs, PeerImp
|
// out: NetworkOPs, PeerImp
|
||||||
@@ -339,6 +348,7 @@ JSS(limit); // in/out: AccountTx*, AccountOffers,
|
|||||||
JSS(limit_peer); // out: AccountLines
|
JSS(limit_peer); // out: AccountLines
|
||||||
JSS(lines); // out: AccountLines
|
JSS(lines); // out: AccountLines
|
||||||
JSS(list); // out: ValidatorList
|
JSS(list); // out: ValidatorList
|
||||||
|
JSS(listener_info); // out: federator_info
|
||||||
JSS(load); // out: NetworkOPs, PeerImp
|
JSS(load); // out: NetworkOPs, PeerImp
|
||||||
JSS(load_base); // out: NetworkOPs
|
JSS(load_base); // out: NetworkOPs
|
||||||
JSS(load_factor); // out: NetworkOPs
|
JSS(load_factor); // out: NetworkOPs
|
||||||
@@ -355,6 +365,7 @@ JSS(local_txs); // out: GetCounts
|
|||||||
JSS(local_static_keys); // out: ValidatorList
|
JSS(local_static_keys); // out: ValidatorList
|
||||||
JSS(lowest_sequence); // out: AccountInfo
|
JSS(lowest_sequence); // out: AccountInfo
|
||||||
JSS(lowest_ticket); // out: AccountInfo
|
JSS(lowest_ticket); // out: AccountInfo
|
||||||
|
JSS(mainchain); // out: federator_info
|
||||||
JSS(majority); // out: RPC feature
|
JSS(majority); // out: RPC feature
|
||||||
JSS(manifest); // out: ValidatorInfo, Manifest
|
JSS(manifest); // out: ValidatorInfo, Manifest
|
||||||
JSS(marker); // in/out: AccountTx, AccountOffers,
|
JSS(marker); // in/out: AccountTx, AccountOffers,
|
||||||
@@ -436,6 +447,7 @@ JSS(peers); // out: InboundLedger, handlers/Peers, Overlay
|
|||||||
JSS(peer_disconnects); // Severed peer connection counter.
|
JSS(peer_disconnects); // Severed peer connection counter.
|
||||||
JSS(peer_disconnects_resources); // Severed peer connections because of
|
JSS(peer_disconnects_resources); // Severed peer connections because of
|
||||||
// excess resource consumption.
|
// excess resource consumption.
|
||||||
|
JSS(pending_transactions); // out: federator_info
|
||||||
JSS(port); // in: Connect
|
JSS(port); // in: Connect
|
||||||
JSS(previous); // out: Reservations
|
JSS(previous); // out: Reservations
|
||||||
JSS(previous_ledger); // out: LedgerPropose
|
JSS(previous_ledger); // out: LedgerPropose
|
||||||
@@ -508,7 +520,9 @@ JSS(server_version); // out: NetworkOPs
|
|||||||
JSS(settle_delay); // out: AccountChannels
|
JSS(settle_delay); // out: AccountChannels
|
||||||
JSS(severity); // in: LogLevel
|
JSS(severity); // in: LogLevel
|
||||||
JSS(shards); // in/out: GetCounts, DownloadShard
|
JSS(shards); // in/out: GetCounts, DownloadShard
|
||||||
|
JSS(sidechain); // out: federator_info
|
||||||
JSS(signature); // out: NetworkOPs, ChannelAuthorize
|
JSS(signature); // out: NetworkOPs, ChannelAuthorize
|
||||||
|
JSS(signatures); // out: federator_info
|
||||||
JSS(signature_verified); // out: ChannelVerify
|
JSS(signature_verified); // out: ChannelVerify
|
||||||
JSS(signing_key); // out: NetworkOPs
|
JSS(signing_key); // out: NetworkOPs
|
||||||
JSS(signing_keys); // out: ValidatorList
|
JSS(signing_keys); // out: ValidatorList
|
||||||
@@ -536,6 +550,7 @@ JSS(sub_index); // in: LedgerEntry
|
|||||||
JSS(subcommand); // in: PathFind
|
JSS(subcommand); // in: PathFind
|
||||||
JSS(success); // rpc
|
JSS(success); // rpc
|
||||||
JSS(supported); // out: AmendmentTableImpl
|
JSS(supported); // out: AmendmentTableImpl
|
||||||
|
JSS(sync_info); // out: federator_info
|
||||||
JSS(system_time_offset); // out: NetworkOPs
|
JSS(system_time_offset); // out: NetworkOPs
|
||||||
JSS(tag); // out: Peers
|
JSS(tag); // out: Peers
|
||||||
JSS(taker); // in: Subscribe, BookOffers
|
JSS(taker); // in: Subscribe, BookOffers
|
||||||
|
|||||||
46
src/ripple/rpc/handlers/FederatorInfo.cpp
Normal file
46
src/ripple/rpc/handlers/FederatorInfo.cpp
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2021 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#include <ripple/app/main/Application.h>
|
||||||
|
#include <ripple/app/sidechain/Federator.h>
|
||||||
|
#include <ripple/json/json_value.h>
|
||||||
|
#include <ripple/net/RPCErr.h>
|
||||||
|
#include <ripple/protocol/jss.h>
|
||||||
|
#include <ripple/rpc/Context.h>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
|
||||||
|
Json::Value
|
||||||
|
doFederatorInfo(RPC::JsonContext& context)
|
||||||
|
{
|
||||||
|
Json::Value ret{Json::objectValue};
|
||||||
|
|
||||||
|
if (auto f = context.app.getSidechainFederator())
|
||||||
|
{
|
||||||
|
ret[jss::info] = f->getInfo();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ret[jss::info] = "Not configured as a sidechain federator";
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ripple
|
||||||
@@ -59,6 +59,8 @@ doDownloadShard(RPC::JsonContext&);
|
|||||||
Json::Value
|
Json::Value
|
||||||
doFeature(RPC::JsonContext&);
|
doFeature(RPC::JsonContext&);
|
||||||
Json::Value
|
Json::Value
|
||||||
|
doFederatorInfo(RPC::JsonContext&);
|
||||||
|
Json::Value
|
||||||
doFee(RPC::JsonContext&);
|
doFee(RPC::JsonContext&);
|
||||||
Json::Value
|
Json::Value
|
||||||
doFetchInfo(RPC::JsonContext&);
|
doFetchInfo(RPC::JsonContext&);
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ Handler const handlerArray[]{
|
|||||||
#endif
|
#endif
|
||||||
{"get_counts", byRef(&doGetCounts), Role::ADMIN, NO_CONDITION},
|
{"get_counts", byRef(&doGetCounts), Role::ADMIN, NO_CONDITION},
|
||||||
{"feature", byRef(&doFeature), Role::ADMIN, NO_CONDITION},
|
{"feature", byRef(&doFeature), Role::ADMIN, NO_CONDITION},
|
||||||
|
{"federator_info", byRef(&doFederatorInfo), Role::USER, NO_CONDITION},
|
||||||
{"fee", byRef(&doFee), Role::USER, NEEDS_CURRENT_LEDGER},
|
{"fee", byRef(&doFee), Role::USER, NEEDS_CURRENT_LEDGER},
|
||||||
{"fetch_info", byRef(&doFetchInfo), Role::ADMIN, NO_CONDITION},
|
{"fetch_info", byRef(&doFetchInfo), Role::ADMIN, NO_CONDITION},
|
||||||
{"ledger_accept",
|
{"ledger_accept",
|
||||||
|
|||||||
@@ -683,14 +683,7 @@ parseRippleLibSeed(Json::Value const& value)
|
|||||||
if (!value.isString())
|
if (!value.isString())
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
auto const result = decodeBase58Token(value.asString(), TokenType::None);
|
return ripple::parseRippleLibSeed(value.asString());
|
||||||
|
|
||||||
if (result.size() == 18 &&
|
|
||||||
static_cast<std::uint8_t>(result[0]) == std::uint8_t(0xE1) &&
|
|
||||||
static_cast<std::uint8_t>(result[1]) == std::uint8_t(0x4B))
|
|
||||||
return Seed(makeSlice(result.substr(2)));
|
|
||||||
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<Seed>
|
std::optional<Seed>
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ ServerHandlerImp::onWSMessage(
|
|||||||
if (size > RPC::Tuning::maxRequestSize ||
|
if (size > RPC::Tuning::maxRequestSize ||
|
||||||
!Json::Reader{}.parse(jv, buffers) || !jv.isObject())
|
!Json::Reader{}.parse(jv, buffers) || !jv.isObject())
|
||||||
{
|
{
|
||||||
|
Json::Reader{}.parse(jv, buffers); // swd debug
|
||||||
Json::Value jvResult(Json::objectValue);
|
Json::Value jvResult(Json::objectValue);
|
||||||
jvResult[jss::type] = jss::error;
|
jvResult[jss::type] = jss::error;
|
||||||
jvResult[jss::error] = "jsonInvalid";
|
jvResult[jss::error] = "jsonInvalid";
|
||||||
|
|||||||
Reference in New Issue
Block a user