Merge monero-lws upstream (full parity: /feed websocket + mempool)

Brings wownero-lws up to date with monero-lws master (+18 commits incl. the /feed
websocket push API and mempool support in get_address_txs). All 10 merge conflicts
resolved; consensus-critical wownero adaptations preserved: decode_amount BulletproofPlus
scalarmult8 + monero-0.18 rct::h2d + rct_type; view tags; PoW next_difficulty signature.

Builds clean against wownero v0.11.4.0 and runtime-verified: daemon connects + scans,
mempool pool-updater runs, /feed wired (--feed-timeout), REST get_address_info returns
valid JSON.

Restores wownero max_ring_size=25 (forced ring 22 / mixin 21 per wownero source);
the March re-port had regressed it to the monero default of 20.

NOTE: needs synced-chain scan testing (esp. BulletproofPlus amount decode) before
promoting to release/master.
This commit is contained in:
jwinterm
2026-06-12 14:26:52 -04:00
63 changed files with 5307 additions and 943 deletions
+8 -8
View File
@@ -38,13 +38,13 @@ jobs:
if: matrix.os == 'macos-latest'
run: ${{env.BREW_INSTALL_MAC}}
- name: Checkout Monero Source
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
repository: monero-project/monero
path: ${{github.workspace}}/monero/
submodules: recursive
- name: Checkout LWS Source
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
path: ${{github.workspace}}/lws
submodules: recursive
@@ -80,13 +80,13 @@ jobs:
dnf config-manager --set-enabled crb
dnf -y install boost-devel cmake gcc g++ git hidapi-devel libsodium-devel make miniupnpc-devel openssl-devel unbound-devel zeromq-devel zlib-devel
- name: Checkout Monero Source
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
repository: monero-project/monero
path: ${{github.workspace}}/monero/
submodules: recursive
- name: Checkout LWS Source
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
path: ${{github.workspace}}/lws
- name: Configure LWS
@@ -121,7 +121,7 @@ jobs:
run: HOMEBREW_NO_AUTO_UPDATE=1 brew install rabbitmq-c
- name: Checkout Monero Source
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
repository: monero-project/monero
path: ${{github.workspace}}/monero/
@@ -134,7 +134,7 @@ jobs:
run: cd ${{github.workspace}}/monero/build && make -j$(nproc) daemon lmdb_lib multisig
- name: Checkout LWS Source
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
path: ${{github.workspace}}/lws
@@ -173,14 +173,14 @@ jobs:
run: HOMEBREW_NO_AUTO_UPDATE=1 brew install rabbitmq-c
- name: Checkout Monero Source
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
repository: monero-project/monero
path: ${{github.workspace}}/monero/
submodules: recursive
- name: Checkout LWS Source
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
path: ${{github.workspace}}/lws
+31 -18
View File
@@ -1,27 +1,40 @@
name: docs
on:
push:
branches: [ "master" ]
paths: [ "docs/**", "mkdocs.yml" ]
permissions:
contents: read
pages: write
id-token: write
jobs:
deploy:
build:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout LWS Source
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v6
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
python3 -m pip install mkdocs
python3 -m pip install mkdocs-swagger-ui-tag
python3 -m pip install mkdocs-schema-reader
mkdocs build
- name: Deploy
uses: peaceiris/actions-gh-pages@v4
- uses: actions/configure-pages@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./site
python-version: 3.x
- run: |
python3 -m pip install mkdocs
python3 -m pip install mkdocs-swagger-ui-tag
python3 -m pip install mkdocs-schema-reader
python3 -m mkdocs build
- uses: actions/upload-pages-artifact@v4
id: deployment
with:
path: site
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+3 -3
View File
@@ -10,14 +10,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v6
- name: Build Docker image
run: docker build --no-cache --tag vtnerd/monero-lws:master .
- name: Add additional tags
run: docker image tag vtnerd/monero-lws:master ghcr.io/vtnerd/monero-lws:master
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -26,7 +26,7 @@ jobs:
run: docker push -a ghcr.io/vtnerd/monero-lws
- name: Login to Docker.io
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ github.actor }}
password: ${{ secrets.DOCKER_VTNERD_TOKEN }}
+8
View File
@@ -57,6 +57,13 @@ if(NOT SANITIZER STREQUAL "OFF")
endif()
endif()
option(LWS_DEAD_FUNCTION_REMOVAL "Attempt gcc dead code elimination" OFF)
if (LWS_DEAD_FUNCTION_REMOVAL)
add_compile_options("-ffunction-sections")
add_compile_options("-fdata-sections")
add_link_options("-Wl,--gc-sections")
endif()
if(STATIC)
if(MSVC)
set(CMAKE_FIND_LIBRARY_SUFFIXES .lib .dll.a .a ${CMAKE_FIND_LIBRARY_SUFFIXES})
@@ -240,6 +247,7 @@ set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
# boost
set(Boost_NO_BOOST_CMAKE ON)
# If Wownero was built statically, we should probably do the same
+258
View File
@@ -0,0 +1,258 @@
# '/feed' API
The `/feed` websocket API allows for faster synchronization on initalization (msgpack
with less redundant data compared to [wallet api](/api/wallet/)), and "push"
updates instead of polling. New block arrivals, new received/spent transactions,
and reorgs are all pushed to the client in real-time. This should provide an
enterprise-like experience where the (usually mobile) client is seamlessly
synchronized with `monerod` (but only if `monero-lws-daemon` is using ZMQ
PUB/SUB from `monerod`).
## Basics
The client initiates a `/feed` websocket connection, then the LWS server sends
the entire tx history, and then finally sends a continous stream of updates from
`monerod` (ZMQ PUB mempool+blocks messages) to the client.
## Flow
### Handshake
The client must attempt a websocket "upgrade" connection to `/feed`. The
initial handshake must have a HTTP/1.1 field, `Sec-WebSocket-Protocol`, which
is set to msgpack or json:
```
Sec-WebSocket-Protocol: lws.feed.v0.msgpack
Sec-WebSocket-Protocol: lws.feed.v0.json
```
This parameter signals the protocol version and payload type. All message
payloads must match the type set in the header. Msgpack is recommended as both
serialization and de-serialization is faster, and the messages are more
compact (especially since binary crypto blobs are not encoded as hex).
### Synchronization
After the websocket handshake, the client must send a [login](#login) message,
and the server must respond with a [tx-sync](#tx-sync) message OR
[error](#error) message. An `error` message signals the websocket is now in the
"terminated" state, a `tx-sync` message indicates that server is now ready to
send real-time updates. In either case, the client shall not send any further
data - the server will terminate the connection if this is not followed.
### Updates
After the `tx-sync`, the server will then push [blocks](#blocks),
[mempool](#mempool), [warning](#warning), or [error](#error) messages until the
stream is terminated. An `error` message indicates the stream is immediately
terminated, whereas the other messages indicate a valid stream where clients
should expected further messages.
## Message Format
Every message has a prefix before the msgpack/json payload that indicates the
type. Current valid prefixes are `login:`, `tx-sync:`, `blocks:`, `mempool:`,
`warning:`, and `error:`. The `login:` prefix is from the client, and the
remainder are sent by the server. The root type of every payload is an object.
As an example, a client login message (in the json sub-protocol), will look
like:
```json
login:{
"account":{
"address": "47nPhxp2cJeKN2NjamupNUNA13XgzcYPzQBCzzsKcj717s8M2UpFVmmdwSuYwgyy8kPDwU7hpEqTTDvfe5LAb9Aj6nwmEzf",
"view_key": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205"
}
}
```
This format (with prefixes) allows for single-pass DOMless parsing.
### login
Messages of this type are sent (only) by the client. The message contains
proof the client has authority to receives updates for a given primary address,
along with optional restrictions on data sent/pushed by the server.
[schema](schemas/feed_login.json)
#### Example
```json
login:{
"account":{
"address": "47nPhxp2cJeKN2NjamupNUNA13XgzcYPzQBCzzsKcj717s8M2UpFVmmdwSuYwgyy8kPDwU7hpEqTTDvfe5LAb9Aj6nwmEzf",
"view_key": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205"
},
"tx_sync": false,
"receives_only": true
}
```
### tx-sync
Messages of this type are sent only by the server, and only in response to a
successful `login:` command from the client. The message contains everything
needed for the client to have a full tx synchronization with the backend
(unless the client requested no transactions).
[schema](schemas/feed_tx_sync.json)
#### Example
```json
tx_sync:{
"scanned_block_height": 10,
"blockchain_height": 10,
"lookahead_fail": 5,
"lookahead": {"maj_i": 50, "min_i": 200 },
"transactions": [
{
"hash": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205",
"prefix_hash": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205",
"timestamp": 0,
"fee": 0,
"unlock_time": 0,
"mixin": 0,
"payment_id": "ee171270296bbc26",
"coinbase": true,
"mempool": true,
"receives": [
{
"id": {"legacy": {"amount": 0, "index": 0}},
"amount": 0,
"public_key": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205",
"index": 0,
"tx_pub_key": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205",
"rct": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205",
"recipient": {"maj_i": 1, "min_i": 1}
}
],
"spends": [
{
"id": {"unified": 0},
"key_image": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205"
}
]
}
]
}
```
### blocks
Messages of this type are sent only by the server when a new block has been
scanned on the account. If another block was added to the Monero chain, this
message type is not sent until a new block has been scanned against the
account. This will also give real-time updates when account is scanning
historical blocks (although the server can choose to group/aggregate messages
to reduce total number of messages).
[schema](schemas/feed_blocks.json)
#### Example
```json
blocks:{
"scan_start": 150,
"scan_end": 151,
"blockchain_height": 151,
"lookahead_fail": 0,
"lookahead": {"maj_i": 50, "min_i": 200 },
"transactions": [
{
"hash": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205",
"prefix_hash": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205",
"timestamp": 0,
"fee": 0,
"unlock_time": 0,
"mixin": 0,
"payment_id": "ee171270296bbc26",
"coinbase": true,
"receives": [
{
"id": {"legacy": {"amount": 0, "index": 0}},
"amount": 0,
"public_key": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205",
"index": 0,
"tx_pub_key": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205",
"rct": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205",
"recipient": {"maj_i": 1, "min_i": 1}
}
],
"spends": [
{
"id": {"unified": 0},
"key_image": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205"
}
]
}
]
}
```
### mempool
If the account scanner has caught-up with the most recent block, the server may
send _received outputs_ that are still in the mempool. These outputs will be
re-sent in the `blocks:` message in a transaction; the ID numbers for outputs
are not assigned until the transaction is included in a block. Spends are
assumed to be under the control of the client, and are not sent in mempool
messages.
[schema](schemas/feed_mempool.json)
#### Full Example
```json
mempool:{
"hash": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205",
"prefix_hash": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205",
"timestamp": 0,
"fee": 0,
"unlock_time": 0,
"mixin": 0,
"payment_id": "ee171270296bbc26",
"amount": 0,
"public_key": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205",
"tx_pub_key": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205",
"index": 0,
"recipient": {"maj_i": 1, "min_i": 1},
"rct": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205"
}
```
### warning
The primary purpose of this message type is to alert the client that the server
has detected a block re-organization. The rollback point is transmitted in the
message, and the client is expected to rollback transactions that are part of
the re-organization. Other events, such as lost connection to `monerod` are also
sent as warnings. Most other events will be [error](#error).
[schema](schemas/feed_warning.json)
#### Example
```json
warning:{
"msg": "Descriptive warning message",
"code": 4,
"counter": 10,
"height": 1000
}
```
#### Codes
```c++
unspecified_error = 0,
account_not_found = 1,
bad_address = 2,
bad_view_key = 3,
blockchain_reorg = 4,
daemon_unresponsive = 5,
parse_error = 6,
protocol_error = 7,
queue_error = 8,
schema_error = 9
```
### error
Servers will send an error of this type when the websocket must be closed; no
further messages are to be sent after sending this type. The codes sent in this
message type are identical to the ones [specified aboved](#codes) for warning
messages.
[schema](schemas/feed_error.json)
#### Example
```json
error:{
"msg": "Descriptive error message",
"code": 8
}
```
+147
View File
@@ -0,0 +1,147 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"$id": "https://docs.monerolws.com/api/schemas/feed_base.json",
"definitions": {
"address_meta": {
"type": "object",
"properties": {
"maj_i": {"$ref": "#/definitions/uint32"},
"min_i": {"$ref": "#/definitions/uint32"}
},
"required": ["maj_i", "min_i"]
},
"binary8": {
"type": "string",
"minLength": 16,
"maxLength": 16,
"pattern": "^[0-9A-Fa-f]{16}$",
"example": "ee171270296bbc26"
},
"binary32": {
"type": "string",
"minLength": 64,
"maxLength": 64,
"pattern": "^[0-9A-Fa-f]{64}$",
"example": "ee171270296bbc26d5be4455b6313e1a2086a92080e205f77d6f861f8e5fd205"
},
"composite_id": {
"anyOf": [
{"$ref": "#/definitions/legacy_id"},
{"$ref": "#/definitions/unified_id"}
]
},
"int64": {
"type": "integer",
"minimum": -9223372036854775808,
"maximum": 9223372036854775807
},
"legacy_id": {
"type": "object",
"properties": {
"legacy": {
"type": "object",
"properties": {
"amount": {"$ref": "#/definitions/uint64"},
"index": {"$ref": "#/definitions/uint64"}
},
"required": ["amount", "index"]
}
},
"required": ["legacy"]
},
"receive" : {
"type": "object",
"properties": {
"amount": {"$ref": "#/definitions/uint64"},
"public_key": {"$ref": "#/definitions/binary32"},
"index": {"$ref": "#/definitions/uint32"},
"tx_pub_key": {"$ref": "#/definitions/binary32"},
"id": {"$ref": "#/definitions/composite_id"},
"rct": {"$ref": "#/definitions/binary32"},
"recipient": {
"$ref": "#/definitions/address_meta",
"default": "{0, 0}"
}
},
"required": [
"amount",
"public_key",
"index",
"tx_pub_key"
]
},
"spend": {
"type": "object",
"properties": {
"id": {"$ref": "#/definitions/composite_id"},
"key_image": {"$ref": "#/definitions/binary32"}
},
"required": ["key_image"]
},
"transaction": {
"type": "object",
"properties": {
"hash": {"$ref": "#/definitions/binary32"},
"prefix_hash": {"$ref": "#/definitions/binary32"},
"timestamp": {"$ref": "#/definitions/int64"},
"fee": {"$ref": "#/definitions/uint64"},
"unlock_time": {"$ref": "#/definitions/uint64"},
"mixin": {"$ref": "#/definitions/uint32"},
"height": {"$ref": "#/definitions/uint64"},
"payment_id": {
"anyOf": [
{"$ref": "#/definitions/binary8"},
{"$ref": "#/definitions/binary32"}
]
},
"coinbase": {
"type": "boolean",
"default": false
},
"mempool": {
"type": "boolean",
"default": false
},
"spends": {
"type": "array",
"minItems": 1,
"items": {"$ref" : "#/definitions/spend"}
},
"receives": {
"type": "array",
"minItems": 1,
"items": {"$ref" : "#/definitions/receive"}
}
},
"required": [
"hash",
"prefix_hash",
"timestamp",
"fee",
"unlock_time"
]
},
"uint16": {
"type": "integer",
"minimum": 0,
"maximum": 65535
},
"uint32": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"uint64": {
"type": "integer",
"minimum": 0,
"maximum": 18446744073709551615
},
"unified_id": {
"type": "object",
"properties": {
"unified": {"$ref" : "#/definitions/uint64"}
},
"required": ["unified"]
}
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"$id": "https://docs.monerolws.com/api/schemas/feed_blocks.json",
"type": "object",
"properties": {
"scan_start": {"$ref": "feed_base.json#/definitions/uint64"},
"scan_end": {"$ref": "feed_base.json#/definitions/uint64"},
"blockchain_height": {"$ref": "feed_base.json#/definitions/uint64"},
"lookahead_fail": {"$ref": "feed_base.json#/definitions/uint64"},
"lookahead": {
"$ref": "feed_base.json#/definitions/address_meta",
"default": "{0, 0}"
},
"transactions": {
"type": "array",
"minItems": 1,
"items": {"$ref": "feed_base.json#/definitions/transaction"}
}
},
"required": ["scan_start", "scan_end", "blockchain_height"]
}
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"$id": "https://docs.monerolws.com/api/schemas/feed_error.json",
"type": "object",
"properties": {
"msg": {"type": "string"},
"code": {"$ref": "feed_base.json#/definitions/uint16"}
},
"required": ["msg", "code"]
}
+36
View File
@@ -0,0 +1,36 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"$id": "https://docs.monerolws.com/api/schemas/feed_login.json",
"type": "object",
"properties": {
"account": {
"type": "object",
"properties": {
"address": {"$ref": "#/definitions/base58-address"},
"view_key": {"$ref": "feed_base.json#/definitions/binary32"}
},
"required": ["address", "view_key"]
},
"tx_sync": {
"description": "Whether to include txes in 'tx_sync:' response",
"type": "boolean",
"default": true
},
"receives_only": {
"description": "Send only received outputs messages after 'tx_sync:' message",
"type": "boolean",
"default": false
}
},
"required": ["account"],
"definitions": {
"base58-address": {
"type": "string",
"minLength": 95,
"maxLength": 95,
"pattern": "^[0-9A-Za-z]{95}$",
"example": "47nPhxp2cJeKN2NjamupNUNA13XgzcYPzQBCzzsKcj717s8M2UpFVmmdwSuYwgyy8kPDwU7hpEqTTDvfe5LAb9Aj6nwmEzf"
}
}
}
+33
View File
@@ -0,0 +1,33 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"$id": "https://docs.monerolws.com/api/schemas/feed_mempool.json",
"type": "object",
"properties": {
"hash": {"$ref": "feed_base.json#/definitions/binary32"},
"prefix_hash": {"$ref": "feed_base.json#/definitions/binary32"},
"fee": {"$ref": "feed_base.json#/definitions/uint64"},
"unlock_time": {"$ref": "feed_base.json#/definitions/uint64"},
"amount": {"$ref": "feed_base.json#/definitions/uint64"},
"public_key": {"$ref": "feed_base.json#/definitions/binary32"},
"tx_pub_key": {"$ref": "feed_base.json#/definitions/binary32"},
"index": {"$ref": "feed_base.json#/definitions/uint32"},
"payment_id": {
"anyOf": [
{"$ref": "feed_base.json#/definitions/binary8"},
{"$ref": "feed_base.json#/definitions/binary32"}
]
},
"mixin": {"$ref": "feed_base.json#/definitions/uint32"},
"recipient": {"$ref": "feed_base.json#/definitions/address_meta"},
"rct": {"$ref": "feed_base.json#/definitions/binary32"}
},
"required": [
"hash",
"prefix_hash",
"fee",
"unlock_time",
"public_key",
"tx_pub_key",
"index"
]
}
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"$id": "https://docs.monerolws.com/api/schemas/feed_tx_sync.json",
"type": "object",
"properties": {
"scanned_block_height": {"$ref": "feed_base.json#/definitions/uint64"},
"blockchain_height": {"$ref": "feed_base.json#/definitions/uint64"},
"lookahead_fail": {"$ref": "feed_base.json#/definitions/uint64"},
"lookahead": {
"$ref": "feed_base.json#/definitions/address_meta",
"default": "{0, 0}"
},
"transactions": {
"type": "array",
"minItems": 1,
"items": {"$ref": "feed_base.json#/definitions/transaction"}
}
},
"required": ["scanned_block_height", "blockchain_height"]
}
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"$id": "https://docs.monerolws.com/api/schemas/feed_warning.json",
"type": "object",
"properties": {
"msg": {"type": "string"},
"code": {"$ref": "feed_base.json#/definitions/uint16"},
"counter": {"$ref": "feed_base.json#/definitions/uint32"},
"height": {"$ref": "feed_base.json#/definitions/uint64"}
},
"required": ["msg", "code", "counter", "height"]
}
+10
View File
@@ -18,6 +18,16 @@ tags:
- name: subaddress
- name: transaction
paths:
/feed:
get:
tags: [transaction]
summary: Check/upgrade to websocket /feed
operationId: feed
responses:
'101':
description: Switching to websocket /feed protocol
'501':
description: /feed is enabled on websocket upgrade only
/get_address_info:
post:
tags:
+1
View File
@@ -19,6 +19,7 @@ nav:
- Wallet: api/wallet.md
- Admin: api/admin.md
- Webhooks/ZeroMQ: api/zmq.md
- Feed: api/feed.md
- RabbitMQ: api/rmq.md
plugins:
- swagger-ui-tag
+1 -1
View File
@@ -72,7 +72,7 @@ set(wownero-lws-common_headers config.h error.h fwd.h)
add_library(wownero-lws-common ${wownero-lws-common_sources} ${wownero-lws-common_headers})
target_link_libraries(wownero-lws-common wownero::libraries)
add_library(wownero-lws-daemon-common rest_server.cpp scanner.cpp)
add_library(wownero-lws-daemon-common rest_server.cpp scanner.cpp mempool.cpp)
target_include_directories(wownero-lws-daemon-common PUBLIC ${ZMQ_INCLUDE_PATH})
target_link_libraries(wownero-lws-daemon-common
PUBLIC
+9 -3
View File
@@ -195,7 +195,7 @@ namespace
{
std::shared_ptr<lws::rpc::scanner::client> client_;
bool operator()(boost::asio::io_context&, lws::rpc::client&, net::http::client&, epee::span<const crypto::hash> chain, epee::span<const lws::account> users, epee::span<const lws::db::pow_sync> pow)
bool operator()(boost::asio::io_context&, lws::rpc::client&, net::http::client&, epee::span<const crypto::hash> chain, epee::span<lws::account> users, epee::span<const lws::db::pow_sync> pow)
{
if (!client_)
return false;
@@ -203,15 +203,21 @@ namespace
return true;
if (!pow.empty() || chain.empty())
return false;
if (!std::is_sorted(users.begin(), users.end(), lws::by_height{}))
return false;
std::vector<crypto::hash> chain_copy{};
chain_copy.reserve(chain.size());
std::copy(chain.begin(), chain.end(), std::back_inserter(chain_copy));
const lws::db::block_id height = lws::db::block_id(to_uint(users[0].scan_height()) + chain.size() - 1);
std::vector<lws::account> users_copy{};
users_copy.reserve(users.size());
for (const auto& user : users)
for (auto& user : users)
{
users_copy.push_back(user.clone());
user.updated(height);
}
MINFO("Processed " << chain.size() << " block(s) against " << users.size() << " account(s)");
lws::rpc::scanner::client::send_update(client_, std::move(users_copy), std::move(chain_copy));
@@ -271,7 +277,7 @@ namespace
void run(program prog)
{
MINFO("Using monerod ZMQ RPC at " << prog.monerod_rpc);
auto ctx = lws::rpc::context::make(std::move(prog.monerod_rpc), std::move(prog.monerod_sub), {}, {}, std::chrono::minutes{0}, false);
auto ctx = lws::rpc::context::make(std::move(prog.monerod_rpc), std::move(prog.monerod_sub), {}, {}, std::chrono::minutes{0}, false, false);
lws::scanner_sync self{epee::net_utils::ssl_verification_t::system_ca};
+4
View File
@@ -1,5 +1,6 @@
#pragma once
#include <chrono>
#include <cstdint>
#include "cryptonote_config.h"
@@ -9,6 +10,9 @@ namespace config
{
extern cryptonote::network_type network;
//! Default inactivity timeout for `/feed` websocket connections
constexpr const std::chrono::minutes feed_timeout{4};
//! Max queued clients when waiting for cacheable daemon response
constexpr const std::size_t daemon_wait_queue_max = 25000;
+25 -3
View File
@@ -51,6 +51,17 @@ namespace lws
return std::memcmp(std::addressof(lhs), std::addressof(rhs), sizeof(lhs)) < 0;
}
};
template<typename F, typename T>
static void map_update(F& format, T& self)
{
wire::object(format,
wire::field<0>("spends", wire::trusted_array(std::ref(self.spends))),
wire::field<1>("outputs", wire::trusted_array(std::ref(self.outputs))),
WIRE_FIELD_ID(3, old_height),
WIRE_FIELD_ID(4, new_height)
);
}
}
struct account::internal
@@ -126,6 +137,15 @@ namespace lws
);
}
void account::update::read_bytes(::wire::msgpack_reader& source)
{ map_update(source, *this); }
void account::update::write_bytes(::wire::msgpack_writer& dest) const
{ map_update(dest, *this); }
void mempool_receive::write_bytes(::wire::msgpack_writer& dest) const
{ map_update(dest, *this); }
account::account() noexcept
: immutable_(nullptr), spendable_(), pubs_(), spends_(), outputs_(), height_(db::block_id(0))
{}
@@ -163,13 +183,15 @@ namespace lws
return result;
}
void account::updated(db::block_id new_height) noexcept
account::update account::updated(db::block_id new_height) noexcept
{
update out{std::move(spends_), std::move(outputs_), height_, new_height};
height_ = std::max(height_, new_height);
spends_.clear();
spends_.shrink_to_fit();
outputs_.clear();
outputs_.shrink_to_fit();
return out;
}
db::account_id account::id() const noexcept
+33 -2
View File
@@ -26,6 +26,7 @@
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <array>
#include <boost/optional/optional.hpp>
#include <cstdint>
#include <memory>
@@ -40,7 +41,25 @@
#include "wire/msgpack/fwd.h"
namespace lws
{
{
//! Sent internallly, same wire format as `account::update`
struct mempool_receive
{
std::array<db::output, 1> outputs;
db::block_id old_height;
db::block_id new_height;
std::array<db::spend, 0> spends;
explicit mempool_receive(const db::output& receive) noexcept
: outputs{{receive}},
old_height(db::block_id::txpool),
new_height(db::block_id::txpool),
spends()
{}
void write_bytes(::wire::msgpack_writer& dest) const;
};
//! Tracks a subset of DB account info for scanning/updating.
class account
{
@@ -61,6 +80,18 @@ namespace lws
public:
//! Sent internally via ZMQ from scanner thread to websocket "push" thread
struct update
{
std::vector<db::spend> spends;
std::vector<db::output> outputs;
db::block_id old_height;
db::block_id new_height;
void read_bytes(::wire::msgpack_reader& source);
void write_bytes(::wire::msgpack_writer& dest) const;
};
//! Construct an "invalid" account (for de-serialization)
account() noexcept;
@@ -89,7 +120,7 @@ namespace lws
account clone() const;
//! \post `height() == max(new_height, height())`, `outputs().empty()`, and `spends.empty()`.
void updated(db::block_id new_height) noexcept;
update updated(db::block_id new_height) noexcept;
//! \return Unique ID from the account database, possibly `db::account_id::kInvalid`.
db::account_id id() const noexcept;
+16
View File
@@ -541,6 +541,22 @@ namespace db
);
}
std::size_t get_hash(account_address const& value) noexcept
{
/* use view_public - it has good distribution and cannot be selected
by user (and has been verified as such at login). */
std::size_t out;
static_assert(sizeof(out) <= sizeof(value.view_public));
static_assert(std::is_trivially_copyable<account_address>::value);
std::memcpy(std::addressof(out), std::addressof(value.view_public), sizeof(out));
return out;
}
bool operator==(account_address const& left, account_address const& right) noexcept
{
static_assert(std::is_trivially_copyable<account_address>::value);
return std::memcmp(std::addressof(left), std::addressof(right), sizeof(left)) == 0;
}
bool operator<(const webhook_dupsort& left, const webhook_dupsort& right) noexcept
{
return left.payment_id == right.payment_id ?
+17
View File
@@ -433,6 +433,13 @@ namespace db
};
void write_bytes(wire::writer&, const webhook_new_account&);
std::size_t get_hash(account_address const& value) noexcept;
bool operator==(account_address const& left, account_address const& right) noexcept;
inline bool operator!=(account_address const& left, account_address const& right) noexcept
{
return !(left == right);
}
inline constexpr bool operator==(address_index const& left, address_index const& right) noexcept
{
return left.maj_i == right.maj_i && left.min_i == right.min_i;
@@ -508,3 +515,13 @@ namespace wire
: std::true_type
{};
}
namespace std
{
template<>
struct hash<lws::db::account_address>
{
std::size_t operator()(lws::db::account_address const& value) const noexcept
{ return lws::db::get_hash(value); }
};
}
+2
View File
@@ -32,6 +32,7 @@
namespace lws
{
class account;
class mempool_receive;
namespace db
{
@@ -56,6 +57,7 @@ namespace db
struct request_info;
struct spend;
class storage;
class storage_reader;
struct subaddress_map;
struct transaction_link;
struct view_key;
+22 -9
View File
@@ -769,7 +769,7 @@ namespace db
const auto v0_requests = requests_v0.open(*txn);
if (v0_requests)
MONERO_UNWRAP(convert_table<v0::request_info, request_info>(*txn, *v0_accounts, tables.requests));
MONERO_UNWRAP(convert_table<v0::request_info, request_info>(*txn, *v0_requests, tables.requests));
else if (v0_requests != lmdb::error(MDB_NOTFOUND))
MONERO_THROW(v0_requests.error(), "Error open old requests table");
@@ -3090,12 +3090,21 @@ namespace db
expect<void> check_spends(std::vector<webhook_tx_spend>& out, MDB_cursor& webhooks_cur, MDB_cursor& outputs_cur, const lws::account& user)
{
const account_id user_id = user.id();
const webhook_key hook_key{user_id, webhook_type::tx_spend};
MDB_val key = lmdb::to_val(hook_key);
MDB_val key = lmdb::to_val(user_id);
MDB_val value{};
int err = mdb_cursor_get(&outputs_cur, &key, &value, MDB_SET_KEY);
if (err)
{
if (err == MDB_NOTFOUND)
return success();
return log_lmdb_error(err, __LINE__, __FILE__);
}
const webhook_key hook_key{user_id, webhook_type::tx_spend};
key = lmdb::to_val(hook_key);
// Find a tx_spend for user id
int err = mdb_cursor_get(&webhooks_cur, &key, &value, MDB_SET_KEY);
err = mdb_cursor_get(&webhooks_cur, &key, &value, MDB_SET_KEY);
for (;;)
{
if (err)
@@ -3111,10 +3120,7 @@ namespace db
out.reserve(user.spends().size());
for (const spend& s : user.spends())
{
key = lmdb::to_val(user_id);
value = lmdb::to_val(s.link.height);
err = mdb_cursor_get(&outputs_cur, &key, &value, MDB_GET_BOTH_RANGE);
err = mdb_cursor_get(&outputs_cur, &key, &value, MDB_FIRST_DUP);
expect<output::spend_meta_> meta{common_error::kInvalidArgument};
for (;;)
{
@@ -3125,7 +3131,14 @@ namespace db
return meta.error();
if (meta->id == s.source)
break;
err = mdb_cursor_get(&outputs_cur, &key, &value, MDB_PREV_DUP);
const auto height = outputs.get_value<MONERO_FIELD(output, link.height)>(value);
if (!height)
return height.error();
if (s.link.height < *height)
return log_lmdb_error(MDB_NOTFOUND, __LINE__, __FILE__);
err = mdb_cursor_get(&outputs_cur, &key, &value, MDB_NEXT_DUP);
}
out.push_back(
+12
View File
@@ -26,6 +26,8 @@
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "error.h"
#include <cstring>
#include <stdexcept>
#include <string>
namespace lws
@@ -144,6 +146,16 @@ namespace lws
}
};
void throw_invalid_argument(const int line, const char* file)
{
if (!file)
file = "";
char const* const end = std::strrchr(file, '/');
if (end)
file = end + 1;
throw std::invalid_argument{"Failed pre-condition at " + std::string{file} + ":" + std::to_string(line)};
}
std::error_category const& error_category() noexcept
{
static const category instance{};
+8
View File
@@ -29,6 +29,13 @@
#include <system_error>
#include <type_traits>
#define LWS_VERIFY(x) \
do \
{ \
if (!(x)) \
::lws::throw_invalid_argument(__LINE__, __FILE__); \
} while (0)
namespace lws
{
enum class error : int
@@ -72,6 +79,7 @@ namespace lws
tx_relay_failed //!< Daemon failed to relayed tx from REST client
};
[[noreturn]] void throw_invalid_argument(int line, const char* file);
std::error_category const& error_category() noexcept;
inline std::error_code make_error_code(lws::error value) noexcept
+1
View File
@@ -30,6 +30,7 @@
namespace lws
{
class account;
class mempool;
class rest_server;
class scanner;
}
+1 -1
View File
@@ -34,7 +34,7 @@ namespace lws { namespace version
constexpr const char branch[] = "@MLWS_COMMIT_BRANCH@";
constexpr const char commit[] = "@MLWS_COMMIT_HASH@";
constexpr const char date[] = "@MLWS_COMMIT_DATE@";
constexpr const char id[] = "1.0-alpha";
constexpr const char id[] = "1.1-alpha";
constexpr const char name[] = "wownero-lws";
// openmonero is currently on 1.6 and we have multiple additions since then
+215
View File
@@ -0,0 +1,215 @@
// Copyright (c) 2024, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "mempool.h"
#include <algorithm>
#include <boost/numeric/conversion/cast.hpp>
#include <boost/thread/lock_guard.hpp>
#include "common/error.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "error.h"
#include "misc_log_ex.h"
#include "rpc/daemon_messages.h" // external/monero/src
#include "rpc/daemon_zmq.h"
#include "rpc/json.h"
#include "util/ownership_test.h"
namespace lws
{
const size_t max_cache_size = 1000;
void mempool::add_txs(epee::span<cryptonote::transaction> txs)
{
const auto now = std::chrono::system_clock::now();
const boost::lock_guard<boost::mutex> lock{mutex_};
snapshot_.reset();
for (auto& tx: txs)
{
auto found = state_.try_emplace(get_transaction_hash(tx), nullptr);
if (!found.first->second)
found.first->second = std::make_shared<pool_entry>(std::move(tx), now);
}
}
void mempool::reset_txs(pool_table&& txs)
{
const boost::lock_guard<boost::mutex> lock{mutex_};
snapshot_.reset();
MDEBUG("Mempool reset from size " << state_.size() << " to " << txs.size());
state_ = std::move(txs);
}
std::unordered_set<crypto::hash> mempool::address_cache::get(const db::account_address& address) const
{
auto it = map_.find(address);
if (it == map_.end())
return {};
return {it->second.txids.begin(), it->second.txids.end()};
}
void mempool::address_cache::set(const db::account_address& address, std::vector<crypto::hash>&& txids)
{
// find or create entry for this address
auto status = map_.emplace(address, entry{});
entry& e = status.first->second;
e.txids = std::move(txids);
if (status.second)
{
// add new address to order list
order_.push_front(address);
e.order = order_.begin();
// evict old address if needed
if (map_.size() >= max_cache_size)
{
auto& evict_address = order_.back();
map_.erase(evict_address);
order_.pop_back();
}
}
else
// move existing address to front of order list
order_.splice(order_.begin(), order_, e.order);
}
std::vector<found_pool_tx> mempool::scan_account(lws::account& user) const
{
std::unordered_set<crypto::hash> skip;
std::shared_ptr<const pool_snapshot> snapshot;
{
const boost::lock_guard<boost::mutex> lock{mutex_};
skip = cache_.get(user.db_address());
if (!snapshot_)
{
// otherwise make a fresh snapshot
pool_snapshot temp;
temp.resize(state_.size());
std::copy(state_.begin(), state_.end(), temp.begin());
snapshot_ = std::make_shared<const pool_snapshot>(std::move(temp));
}
snapshot = snapshot_;
}
std::vector<found_pool_tx> found{};
ownership_test scan_transaction{
[&found](account&, db::spend&& spend)
{
if (found.empty() || found.back().hash != spend.link.tx_hash)
found.push_back({spend.link.tx_hash});
found.back().spends.push_back(std::move(spend));
},
[&found](account&, db::output&& output)
{
if (found.empty() || found.back().hash != output.link.tx_hash)
found.push_back({output.link.tx_hash});
found.back().outputs.push_back(std::move(output));
}
};
// uint64::max is for txpool
static const std::vector<std::uint64_t> fake_outs(
256, std::numeric_limits<std::uint64_t>::max()
);
std::vector<crypto::hash> skipped;
for (const auto& pair: *snapshot) {
if (!skip.count(pair.first))
scan_transaction(
epee::span<lws::account>(&user, 1),
db::block_id::txpool,
boost::numeric_cast<std::uint64_t>(std::chrono::system_clock::to_time_t(pair.second->timestamp)),
std::addressof(pair.first),
pair.second->tx,
fake_outs
);
if (found.empty() || found.back().hash != pair.first)
skipped.push_back(pair.first);
}
{
const boost::lock_guard<boost::mutex> lock{mutex_};
cache_.set(user.db_address(), std::move(skipped));
}
return found;
}
expect<void> pool_update_loop(std::shared_ptr<mempool> pool, rpc::client& client)
{
LWS_VERIFY(pool);
return client.event_loop(
[pool](std::string&& json) -> expect<void>
{
auto msg = rpc::parse_json_response<rpc::get_transaction_pool>(std::move(json));
if (!msg)
{
MERROR("Pool failed to parse block response" << msg.error());
return msg.error();
}
mempool::pool_table txs{};
txs.reserve(msg->transactions.size());
const auto now = std::chrono::system_clock::now();
for (auto& tx: msg->transactions)
{
auto shared_tx = std::make_shared<mempool::pool_entry>(std::move(tx.tx), now);
txs.emplace(tx.tx_hash, std::move(shared_tx));
}
pool->reset_txs(std::move(txs));
return {};
},
[&client](rpc::minimal_chain_pub&&) -> expect<void>
{
cryptonote::rpc::GetTransactionPool::Request req{};
expect<void> status = success();
for (unsigned i = 0; i < 2; ++i)
{
status = client.send(
rpc::client::make_message("get_transaction_pool", req), std::chrono::seconds(0)
);
if (status || status != net::zmq::make_error_code(EFSM))
return status;
MONERO_CHECK(client.daemon_reconnect());
}
return status;
},
[pool](rpc::full_txpool_pub&& msg) -> expect<void>
{
pool->add_txs(epee::to_mut_span(msg.txes));
return {};
}
);
}
} // namespace lws
+116
View File
@@ -0,0 +1,116 @@
// Copyright (c) 2024, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <boost/thread/mutex.hpp>
#include <chrono>
#include <memory>
#include <unordered_map>
#include <vector>
#include "common/expect.h" // external/monero/src
#include "cryptonote_basic/cryptonote_basic.h" // external/monero/src
#include "db/data.h"
#include "db/fwd.h"
#include "rpc/client.h"
namespace lws
{
struct found_pool_tx {
crypto::hash hash;
std::vector<db::spend> spends;
std::vector<db::output> outputs;
};
/*!
Thread-safe mempool cache, designed to perform well with a single writer
and multiple concurrent readers.
The primary mutable state is guarded with a mutex, while each reader
receives an immutable snapshot of the latest state. These snapshots
use structural sharing to avoid copying the heavyweight transactions.
The two update methods are `add_tx` and `filter_txs`.
*/
class mempool
{
public:
struct pool_entry
{
pool_entry(cryptonote::transaction&& tx, const std::chrono::system_clock::time_point timestamp)
: tx(std::move(tx)), timestamp(timestamp)
{}
cryptonote::transaction tx;
std::chrono::system_clock::time_point timestamp;
};
using pool_snapshot = std::vector<std::pair<crypto::hash, std::shared_ptr<const pool_entry>>>;
using pool_table = std::unordered_map<crypto::hash, std::shared_ptr<pool_entry>>;
//! Adds transactions to the pool, if they are not already present.
// Thread-safe.
void add_txs(epee::span<cryptonote::transaction>);
//! Replaces all transactions to the pool. Thread-safe.
void reset_txs(pool_table&&);
//! Scans the pool for transactions belonging to an address. Thread-safe.
std::vector<found_pool_tx> scan_account(lws::account& user) const;
private:
//! Remembers which txid's that do *not* belong to an address,
// since there is no need for the scanner to revisit these.
class address_cache
{
public:
std::unordered_set<crypto::hash> get(const db::account_address& address) const;
void set(const db::account_address& address, std::vector<crypto::hash>&& txids);
private:
struct entry
{
std::vector<crypto::hash> txids;
std::list<db::account_address>::iterator order;
};
std::unordered_map<db::account_address, entry> map_;
std::list<db::account_address> order_;
};
pool_table state_;
mutable address_cache cache_;
mutable std::shared_ptr<const pool_snapshot> snapshot_;
mutable boost::mutex mutex_;
};
//! Run this on a thread to keep the pool in sync.
// Loops until an error or shutdown signal occurs.
expect<void> pool_update_loop(std::shared_ptr<mempool> pool, rpc::client& client);
} // namespace lws
+105 -236
View File
@@ -47,6 +47,7 @@
#include <boost/beast/http/verb.hpp>
#include <boost/beast/http/write.hpp>
#include <boost/beast/version.hpp>
#include <boost/beast/websocket/rfc6455.hpp>
#include <boost/optional/optional.hpp>
#include <boost/range/counting_range.hpp>
#include <boost/thread/lock_guard.hpp>
@@ -64,12 +65,14 @@
#include "common/expect.h" // monero/src
#include "config.h"
#include "crypto/crypto.h" // monero/src
#include "cryptonote_basic/cryptonote_format_utils.h" // monero/srcqq
#include "cryptonote_config.h" // monero/src
#include "db/data.h"
#include "db/storage.h"
#include "db/string.h"
#include "error.h"
#include "lmdb/util.h" // monero/src
#include "mempool.h"
#include "net/http/client.h"
#include "net/http/slice_body.h"
#include "net/net_parse_helpers.h" // monero/contrib/epee/include
@@ -80,10 +83,14 @@
#include "rpc/admin.h"
#include "rpc/client.h"
#include "rpc/daemon_messages.h" // monero/src
#include "rpc/feed.h"
#include "rpc/light_wallet.h"
#include "rpc/login.h"
#include "rpc/rates.h"
#include "rpc/webhook.h"
#include "string_tools.h" // monero/contrib/epee/include
#include "util/gamma_picker.h"
#include "util/ownership_test.h"
#include "util/random_outputs.h"
#include "util/source_location.h"
#include "wire/adapted/crypto.h"
@@ -93,11 +100,21 @@ namespace lws
{
struct runtime_options
{
const std::chrono::seconds feed_timeout;
const std::uint32_t max_subaddresses;
const epee::net_utils::ssl_verification_t webhook_verify;
const bool disable_admin_auth;
const bool auto_accept_creation;
const bool auto_accept_import;
explicit runtime_options(const rest_server::configuration& config)
: feed_timeout(config.feed_timeout),
max_subaddresses(config.max_subaddresses),
webhook_verify(config.webhook_verify),
disable_admin_auth(config.disable_admin_auth),
auto_accept_creation(config.auto_accept_creation),
auto_accept_import(config.auto_accept_import)
{}
};
struct rest_server_data
@@ -105,15 +122,17 @@ namespace lws
boost::asio::io_context io;
const db::storage disk;
const rpc::client client;
const std::shared_ptr<lws::mempool> mempool;
const runtime_options options;
std::vector<net::zmq::async_client> clients;
net::http::client webhook_client;
boost::mutex sync;
rest_server_data(db::storage disk, rpc::client client, runtime_options options)
rest_server_data(db::storage disk, rpc::client client, std::shared_ptr<lws::mempool> mempool, runtime_options options)
: io(),
disk(std::move(disk)),
client(std::move(client)),
mempool(std::move(mempool)),
options(std::move(options)),
webhook_client(options.webhook_verify),
clients(),
@@ -154,7 +173,11 @@ namespace lws
//! `/daemon_status` and `get_unspent_outs` caches ZMQ result for this long
constexpr const std::chrono::seconds daemon_cache_timeout{5};
constexpr const unsigned max_ring_size = 20;
//! Max outputs requestable per `get_random_outs`. Deliberately ABOVE wownero's
//! forced ring size of 22 (mixin 21, enforced min==max in wownero consensus):
//! the wallet requests a few extra decoys so it can discard any unusable ones
//! and still assemble a valid ring. Do NOT lower this to the ring size.
constexpr const unsigned max_ring_size = 25;
constexpr const unsigned max_rings = 150;
struct connection_data
@@ -207,65 +230,7 @@ namespace lws
return to_uint(tx_height) + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > to_uint(last);
}
std::vector<db::output::spend_meta_>::const_iterator
find_metadata(std::vector<db::output::spend_meta_> const& metas, db::output_id id)
{
struct by_output_id
{
bool operator()(db::output::spend_meta_ const& left, db::output_id right) const noexcept
{
return left.id < right;
}
bool operator()(db::output_id left, db::output::spend_meta_ const& right) const noexcept
{
return left < right.id;
}
};
return std::lower_bound(metas.begin(), metas.end(), id, by_output_id{});
}
bool is_hidden(db::account_status status) noexcept
{
switch (status)
{
case db::account_status::active:
case db::account_status::inactive:
return false;
default:
case db::account_status::hidden:
break;
}
return true;
}
bool key_check(const rpc::account_credentials& creds)
{
crypto::public_key verify{};
if (!crypto::secret_key_to_public_key(creds.key, verify))
return false;
if (verify != creds.address.view_public)
return false;
return true;
}
//! \return Account info from the DB, iff key matches address AND address is NOT hidden.
expect<std::pair<db::account, db::storage_reader>> open_account(const rpc::account_credentials& creds, db::storage disk)
{
if (!key_check(creds))
return {lws::error::bad_view_key};
auto reader = disk.start_read();
if (!reader)
return reader.error();
const auto user = reader->get_account(creds.address);
if (!user)
return user.error();
if (is_hidden(user->first))
return {lws::error::account_not_found};
return {std::make_pair(user->second, std::move(*reader))};
}
bool check_lookahead(connection_data& data, const db::address_index lookahead)
{
const auto minor = to_uint(lookahead.min_i);
@@ -514,7 +479,7 @@ namespace lws
static expect<response> handle(const request& req, connection_data& data, std::function<async_complete>&&)
{
auto user = open_account(req, data.global->disk.clone());
auto user = lws::rpc::open_account(req, data.global->disk);
if (!user)
return user.error();
@@ -553,7 +518,7 @@ namespace lws
if (metas.empty() || metas.back().id < meta.id)
metas.push_back(meta);
else
metas.insert(find_metadata(metas, meta.id), meta);
metas.insert(rpc::get_address_txs_response::find_metadata(metas, meta.id), meta);
resp.total_received = rpc::safe_uint64(std::uint64_t(resp.total_received) + meta.amount);
if (is_locked(output.get_value<MONERO_FIELD(db::output, unlock_time)>(), last->id, output.get_value<MONERO_FIELD(db::output, link)>().height))
@@ -563,7 +528,7 @@ namespace lws
resp.spent_outputs.reserve(spends->count());
for (auto const& spend : spends->make_range())
{
const auto meta = find_metadata(metas, spend.source);
const auto meta = rpc::get_address_txs_response::find_metadata(metas, spend.source);
if (meta == metas.end() || meta->id != spend.source)
{
throw std::logic_error{
@@ -591,122 +556,12 @@ namespace lws
static expect<response> handle(const request& req, connection_data& data, std::function<async_complete>&&)
{
auto user = open_account(req, data.global->disk.clone());
auto user = lws::rpc::open_account(req, data.global->disk);
if (!user)
return user.error();
data.passed_login = true;
auto outputs = user->second.get_outputs(user->first.id);
if (!outputs)
return outputs.error();
auto spends = user->second.get_spends(user->first.id);
if (!spends)
return spends.error();
const expect<db::block_info> last = user->second.get_last_block();
if (!last)
return last.error();
response resp{};
resp.scanned_height = std::uint64_t(user->first.scan_height);
resp.scanned_block_height = resp.scanned_height;
resp.start_height = std::uint64_t(user->first.start_height);
resp.blockchain_height = std::uint64_t(last->id);
resp.transaction_height = resp.blockchain_height;
resp.lookahead_fail = to_uint(user->first.lookahead_fail);
resp.lookahead = user->first.lookahead;
// merge input and output info into a single set of txes.
auto output = outputs->make_iterator();
auto spend = spends->make_iterator();
std::vector<db::output::spend_meta_> metas{};
resp.transactions.reserve(outputs->count());
metas.reserve(resp.transactions.capacity());
db::transaction_link next_output{};
db::transaction_link next_spend{};
if (!output.is_end())
next_output = output.get_value<MONERO_FIELD(db::output, link)>();
if (!spend.is_end())
next_spend = spend.get_value<MONERO_FIELD(db::spend, link)>();
while (!output.is_end() || !spend.is_end())
{
if (!resp.transactions.empty())
{
db::transaction_link const& last = resp.transactions.back().info.link;
if ((!output.is_end() && next_output < last) || (!spend.is_end() && next_spend < last))
{
throw std::logic_error{"DB has unexpected sort order"};
}
}
if (spend.is_end() || (!output.is_end() && next_output <= next_spend))
{
std::uint64_t amount = 0;
if (resp.transactions.empty() || resp.transactions.back().info.link.tx_hash != next_output.tx_hash)
{
resp.transactions.push_back({*output});
amount = resp.transactions.back().info.spend_meta.amount;
}
else
{
amount = output.get_value<MONERO_FIELD(db::output, spend_meta.amount)>();
resp.transactions.back().info.spend_meta.amount += amount;
}
const db::output::spend_meta_ meta = output.get_value<MONERO_FIELD(db::output, spend_meta)>();
if (metas.empty() || metas.back().id < meta.id)
metas.push_back(meta);
else
metas.insert(find_metadata(metas, meta.id), meta);
resp.total_received = rpc::safe_uint64(std::uint64_t(resp.total_received) + amount);
++output;
if (!output.is_end())
next_output = output.get_value<MONERO_FIELD(db::output, link)>();
}
else if (output.is_end() || (next_spend < next_output))
{
const db::output_id source_id = spend.get_value<MONERO_FIELD(db::spend, source)>();
const auto meta = find_metadata(metas, source_id);
if (meta == metas.end() || meta->id != source_id)
{
throw std::logic_error{
"Serious database error, no receive for spend"
};
}
if (resp.transactions.empty() || resp.transactions.back().info.link.tx_hash != next_spend.tx_hash)
{
resp.transactions.push_back({});
resp.transactions.back().spends.push_back({*meta, *spend});
resp.transactions.back().info.link.height = resp.transactions.back().spends.back().possible_spend.link.height;
resp.transactions.back().info.link.tx_hash = resp.transactions.back().spends.back().possible_spend.link.tx_hash;
resp.transactions.back().info.spend_meta.mixin_count =
resp.transactions.back().spends.back().possible_spend.mixin_count;
resp.transactions.back().info.timestamp = resp.transactions.back().spends.back().possible_spend.timestamp;
resp.transactions.back().info.unlock_time = resp.transactions.back().spends.back().possible_spend.unlock_time;
}
else
resp.transactions.back().spends.push_back({*meta, *spend});
resp.transactions.back().spent += meta->amount;
++spend;
if (!spend.is_end())
next_spend = spend.get_value<MONERO_FIELD(db::spend, link)>();
}
}
return resp;
return response::load(user->second, user->first, data.global->mempool.get(), false);
}
};
@@ -1066,7 +921,7 @@ namespace lws
static expect<response> handle(request const& req, connection_data& data, std::function<async_complete>&&)
{
auto user = open_account(req, data.global->disk.clone());
auto user = lws::rpc::open_account(req, data.global->disk);
if (!user)
return user.error();
@@ -1090,7 +945,7 @@ namespace lws
if (!rpc)
return rpc.error();
auto user = open_account(req.creds, std::move(disk));
auto user = lws::rpc::open_account(req.creds, std::move(disk));
if (!user)
return user.error();
@@ -1370,7 +1225,7 @@ namespace lws
bool fulfilled = false;
db::address_index lookahead{};
{
auto user = open_account(req.creds, data.global->disk.clone());
auto user = lws::rpc::open_account(req.creds, data.global->disk);
if (!user)
return user.error();
@@ -1453,7 +1308,7 @@ namespace lws
static expect<response> handle(request req, connection_data& data, std::function<async_complete>&&)
{
if (!key_check(req.creds))
if (!lws::rpc::key_check(req.creds))
return {lws::error::bad_view_key};
auto disk = data.global->disk.clone();
@@ -1467,7 +1322,7 @@ namespace lws
if (account)
{
if (is_hidden(account->first))
if (lws::rpc::is_hidden(account->first))
return {lws::error::account_not_found};
// Do not count a request for account creation as login
@@ -1526,7 +1381,7 @@ namespace lws
db::account_id id = db::account_id::invalid;
{
auto user = open_account(req.creds, data.global->disk.clone());
auto user = lws::rpc::open_account(req.creds, data.global->disk);
if (!user)
return user.error();
id = user->first.id;
@@ -1588,6 +1443,10 @@ namespace lws
{
using transaction_rpc = cryptonote::rpc::SendRawTxHex;
cryptonote::blobdata tx_blob;
if (!epee::string_tools::parse_hexstr_to_binbuff(req.tx, tx_blob))
return {lws::error::bad_client_tx};
struct frame
{
rest_server_data* parent;
@@ -1595,7 +1454,7 @@ namespace lws
net::zmq::async_client client;
boost::asio::steady_timer timer;
boost::asio::io_context::strand strand;
std::deque<std::pair<epee::byte_slice, std::function<async_complete>>> resumers;
std::deque<std::tuple<epee::byte_slice, std::function<async_complete>, cryptonote::transaction>> resumers;
std::size_t outstanding;
frame(rest_server_data& parent, net::zmq::async_client client)
@@ -1635,7 +1494,11 @@ namespace lws
return {error::load};
active->outstanding += msg.size();
active->resumers.emplace_back(std::move(msg), std::move(resume));
auto& elem = active->resumers.emplace_back();
std::get<0>(elem) = std::move(msg);
std::get<1>(elem) = std::move(resume);
if (!cryptonote::parse_and_validate_tx_from_blob(tx_blob, std::get<2>(elem)))
return {lws::error::bad_client_tx};
return success();
}
@@ -1652,7 +1515,7 @@ namespace lws
assert(self_ != nullptr);
assert(self_->strand.running_in_this_thread());
std::deque<std::pair<epee::byte_slice, std::function<async_complete>>> resumers;
std::deque<std::tuple<epee::byte_slice, std::function<async_complete>, cryptonote::transaction>> resumers;
{
const boost::lock_guard<boost::mutex> lock{cache.sync};
if (error)
@@ -1665,6 +1528,9 @@ namespace lws
}
else
{
if (self_->parent && self_->parent->mempool)
self_->parent->mempool->add_txs({std::addressof(std::get<2>(self_->resumers.front())), 1});
MDEBUG("Completed ZMQ request in /submit_raw_tx");
resumers.push_back(std::move(self_->resumers.front()));
self_->resumers.pop_front();
@@ -1672,7 +1538,7 @@ namespace lws
}
for (const auto& r : resumers)
r.second(value);
std::get<1>(r)(value);
}
bool set_timeout(std::chrono::steady_clock::duration timeout, const bool expecting) const
@@ -1723,7 +1589,7 @@ namespace lws
self_->parent->store_async_client(std::move(self_->client));
return;
}
next = std::move(self_->resumers.front().first);
next = std::move(std::get<0>(self_->resumers.front()));
self_->outstanding -= std::min(self_->outstanding, next.size());
}
@@ -1768,7 +1634,11 @@ namespace lws
cache.status = active;
active->outstanding += msg.size();
active->resumers.emplace_back(std::move(msg), std::move(resume));
auto& elem = active->resumers.emplace_back();
std::get<0>(elem) = std::move(msg);
std::get<1>(elem) = std::move(resume);
if (!cryptonote::parse_and_validate_tx_from_blob(tx_blob, std::get<2>(elem)))
return {lws::error::bad_client_tx};
lock.unlock();
MDEBUG("Starting new ZMQ request in /submit_raw_tx");
@@ -1789,7 +1659,7 @@ namespace lws
db::account_id id = db::account_id::invalid;
{
auto user = open_account(req.creds, data.global->disk.clone());
auto user = lws::rpc::open_account(req.creds, data.global->disk);
if (!user)
return user.error();
id = user->first.id;
@@ -1922,10 +1792,11 @@ namespace lws
current_max = std::max(current_max, start->max_size);
return current_max;
}
constexpr const endpoint endpoints[] =
{
{"/daemon_status", call<daemon_status>, 1024, true},
{"/feed", nullptr, 0, false},
{"/get_address_info", call<get_address_info>, 2 * 1024, false},
{"/get_address_txs", call<get_address_txs>, 2 * 1024, false},
{"/get_random_outs", call<get_random_outs>, 2 * 1024, true},
@@ -1963,6 +1834,18 @@ namespace lws
constexpr const unsigned max_endpoint_size =
std::max(max_standard_endpoint_size, max_admin_endpoint_size);
constexpr const endpoint* get_endpoint(const std::string_view name) noexcept
{
const endpoint* current = std::begin(endpoints);
endpoint const* const end = std::end(endpoints);
for (; current != end; ++current)
{
if (current->name == name)
break;
}
return current;
}
struct by_name_
{
bool operator()(endpoint const& left, endpoint const& right) const noexcept
@@ -1985,39 +1868,6 @@ namespace lws
}
};
constexpr const by_name_ by_name{};
struct slice_body
{
using value_type = epee::byte_slice;
static std::size_t size(const value_type& source) noexcept
{
return source.size();
}
struct writer
{
epee::byte_slice body_;
using const_buffers_type = boost::asio::const_buffer;
template<bool is_request, typename Fields>
explicit writer(boost::beast::http::header<is_request, Fields> const&, value_type const& body)
: body_(body.clone())
{}
void init(boost::beast::error_code& ec)
{
ec = {};
}
boost::optional<std::pair<const_buffers_type, bool>> get(boost::beast::error_code& ec)
{
ec = {};
return {{const_buffers_type{body_.data(), body_.size()}, false}};
}
};
};
} // anonymous
struct rest_server::internal
@@ -2232,12 +2082,29 @@ namespace lws
const auto target = self_->parser_.value().get().target();
MDEBUG("Received HTTP request from " << self_.get() << " to target " << target);
// Checked access for `parser_` on first use
endpoint const* const handler = self_->parent_->get_endpoint(target);
if (!handler)
return self_->bad_request(boost::beast::http::status::not_found, std::forward<F>(resume));
static constexpr endpoint const* const feed = get_endpoint("/feed");
if (handler == feed)
{
rest_server_data* const from = self_->data_.global;
const auto feed_timeout = from->options.feed_timeout;
const bool feed_disabled = feed_timeout <= std::chrono::seconds{0};
if (!feed_disabled && boost::beast::websocket::is_upgrade(self_->parser_->get()))
{
MDEBUG("Attempting websocket '" << feed->name << "' on " << self_.get());
if (!rpc::feed::start(std::move(self_->sock()), from->io, from->client, from->mempool, self_->parser_->get(), from->disk, feed_timeout))
return self_->bad_request(boost::beast::http::status::bad_request, std::forward<F>(resume));
return; // else a websocket has I/O queued on `io`.
}
else if (feed_disabled)
return self_->bad_request(boost::beast::http::status::not_found, std::forward<F>(resume));
}
if (handler->run == nullptr)
return self_->bad_request(boost::beast::http::status::not_implemented, std::forward<F>(resume));
@@ -2261,17 +2128,19 @@ namespace lws
by the handler function), and therefore needs to be "wrapped" to
ensure thread safety. This also allows `resume` to be unwrapped.
DO NOT use `boost::asio::bind_executor` here as it doesn't create
a new callable like `wrap` does. */
a new callable like `wrap` does (which is deprecated, see dispatch
below). */
const auto& self = self_;
resumer = self->strand_.wrap(
[self, resume] (expect<copyable_slice> body) mutable
{
if (!body)
self->bad_request(body.error(), std::move(resume));
else
self->valid_request(std::move(body->value), std::move(resume));
}
);
resumer = [self, resume] (expect<copyable_slice> body) mutable
{
boost::asio::dispatch(self->strand_, [self, resume = std::move(resume), body = std::move(body)] () mutable
{
if (!body)
self->bad_request(body.error(), std::move(resume));
else
self->valid_request(std::move(body->value), std::move(resume));
});
};
}
MDEBUG("Running REST handler " << handler->name << " on " << self_.get());
@@ -2398,8 +2267,8 @@ namespace lws
}
}
rest_server::rest_server(epee::span<const std::string> addresses, std::vector<std::string> admin, db::storage disk, rpc::client client, configuration config)
: global_(std::make_unique<rest_server_data>(std::move(disk), std::move(client), runtime_options{config.max_subaddresses, config.webhook_verify, config.disable_admin_auth, config.auto_accept_creation, config.auto_accept_import})),
rest_server::rest_server(epee::span<const std::string> addresses, std::vector<std::string> admin, db::storage disk, rpc::client client, std::shared_ptr<lws::mempool> mempool, configuration config)
: global_(std::make_unique<rest_server_data>(std::move(disk), std::move(client), std::move(mempool), runtime_options{config})),
ports_(),
workers_()
{
+9 -1
View File
@@ -41,6 +41,7 @@
namespace lws
{
class mempool;
struct rest_server_data;
class rest_server
{
@@ -60,6 +61,7 @@ namespace lws
{
epee::net_utils::ssl_authentication_t auth;
std::vector<std::string> access_controls;
std::chrono::seconds feed_timeout;
std::size_t threads;
std::uint32_t max_subaddresses;
epee::net_utils::ssl_verification_t webhook_verify;
@@ -69,7 +71,13 @@ namespace lws
bool auto_accept_import;
};
explicit rest_server(epee::span<const std::string> addresses, std::vector<std::string> admin, db::storage disk, rpc::client client, configuration config);
explicit rest_server(
epee::span<const std::string> addresses,
std::vector<std::string> admin,
db::storage disk,
rpc::client client,
std::shared_ptr<lws::mempool> mempool,
configuration config);
rest_server(rest_server&&) = delete;
rest_server(rest_server const&) = delete;
+3 -3
View File
@@ -29,9 +29,9 @@
add_subdirectory(scanner)
set(wownero-lws-rpc_sources admin.cpp client.cpp daemon_pub.cpp daemon_zmq.cpp light_wallet.cpp lws_pub.cpp rates.cpp)
set(wownero-lws-rpc_headers admin.h client.h daemon_pub.h daemon_zmq.h fwd.h json.h light_wallet.h lws_pub.h rates.h)
set(wownero-lws-rpc_sources admin.cpp client.cpp daemon_pub.cpp daemon_zmq.cpp feed_ssl.cpp feed_tcp.cpp light_wallet.cpp login.cpp lws_pub.cpp rates.cpp)
set(wownero-lws-rpc_headers admin.h client.h daemon_pub.h daemon_zmq.h feed.h fwd.h json.h light_wallet.h login.h lws_pub.h rates.h)
add_library(wownero-lws-rpc ${wownero-lws-rpc_sources} ${wownero-lws-rpc_headers})
target_include_directories(wownero-lws-rpc PRIVATE ${RMQ_INCLUDE_DIR})
target_link_libraries(wownero-lws-rpc wownero::libraries wownero-lws-util wownero-lws-wire-json wownero-lws-wire-wrapper ${RMQ_LIBRARY})
target_link_libraries(wownero-lws-rpc wownero::libraries wownero-lws-db wownero-lws-util wownero-lws-wire-json wownero-lws-wire-wrapper ${RMQ_LIBRARY})
+276 -4
View File
@@ -27,6 +27,7 @@
#include "client.h"
#include <array>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/utility/string_ref.hpp>
@@ -35,13 +36,16 @@
#include "common/error.h" // monero/contrib/epee/include
#include "db/account.h"
#include "db/data.h"
#include "error.h"
#include "misc_log_ex.h" // monero/contrib/epee/include
#include "net/http_client.h" // monero/contrib/epee/include
#include "net/zmq.h" // monero/src
#include "net/zmq_async.h"
#include "rpc/light_wallet.h"
#include "scanner.h"
#include "serialization/json_object.h" // monero/src
#include "wire/msgpack/write.h"
#include "wire/json/write.h"
#if MLWS_RMQ_ENABLED
#include <amqp.h>
@@ -66,7 +70,7 @@ namespace rpc
namespace
{
constexpr const char signal_endpoint[] = "inproc://signal";
constexpr const char account_endpoint[] = "inproc://account"; // append integer every new `account_push`
constexpr const char account_endpoint[] = "inproc://account";
constexpr const char abort_scan_signal[] = "SCAN";
constexpr const char abort_process_signal[] = "PROCESS";
constexpr const char minimal_chain_topic[] = "json-minimal-chain_main";
@@ -268,10 +272,11 @@ namespace rpc
{
struct context
{
explicit context(zcontext comm, net::zmq::socket signal_pub, net::zmq::socket external_pub, rcontext rmq_src, std::string daemon_addr, std::string sub_addr, std::chrono::minutes interval, bool untrusted_daemon)
explicit context(zcontext comm, net::zmq::socket signal_pub, net::zmq::socket external_pub, net::zmq::socket internal_pub, rcontext rmq_src, std::string daemon_addr, std::string sub_addr, std::chrono::minutes interval, bool untrusted_daemon)
: comm(std::move(comm))
, signal_pub(std::move(signal_pub))
, external_pub(std::move(external_pub))
, internal_pub(std::move(internal_pub))
, rmq(std::move(rmq_src))
, daemon_addr(std::move(daemon_addr))
, sub_addr(std::move(sub_addr))
@@ -279,8 +284,10 @@ namespace rpc
, cache_time()
, cache_interval(interval)
, cached{}
, warning_count(0)
, sync_pub()
, sync_rates()
, sync_internal_pub()
, untrusted_daemon(untrusted_daemon)
, rates_running()
{
@@ -292,6 +299,7 @@ namespace rpc
zcontext comm;
net::zmq::socket signal_pub;
net::zmq::socket external_pub;
const net::zmq::socket internal_pub;
rcontext rmq;
const std::string daemon_addr;
const std::string sub_addr;
@@ -299,8 +307,10 @@ namespace rpc
std::chrono::steady_clock::time_point cache_time;
const std::chrono::minutes cache_interval;
rates cached;
std::atomic<std::uint32_t> warning_count;
boost::mutex sync_pub;
boost::mutex sync_rates;
boost::mutex sync_internal_pub;
const bool untrusted_daemon;
std::atomic_flag rates_running;
};
@@ -328,6 +338,18 @@ namespace rpc
return {lws::error::bad_daemon_response};
}
expect<std::uint32_t> account_sub::watch(const std::string_view address) const
{
MONERO_PRECOND(ctx);
MONERO_PRECOND(sub.zsock);
const std::string_view warning = feed_warning::prefix();
const std::uint32_t count = ctx->warning_count;
MONERO_ZMQ_CHECK(zmq_setsockopt(sub.zsock.get(), ZMQ_SUBSCRIBE, warning.data(), warning.size()));
MONERO_ZMQ_CHECK(zmq_setsockopt(sub.zsock.get(), ZMQ_SUBSCRIBE, address.data(), address.size()));
return count;
}
expect<net::zmq::socket> client::make_daemon(const std::shared_ptr<detail::context>& ctx) noexcept
{
assert(ctx != nullptr);
@@ -412,6 +434,15 @@ namespace rpc
return ctx && (ctx->external_pub || ctx->rmq.is_available());
}
expect<void> client::daemon_reconnect() noexcept
{
auto new_daemon = make_daemon(ctx);
if (!new_daemon)
return new_daemon.error();
daemon = std::move(*new_daemon);
return success();
}
expect<void> client::watch_scan_signals() noexcept
{
MONERO_PRECOND(ctx != nullptr);
@@ -481,6 +512,124 @@ namespace rpc
return {lws::error::bad_daemon_response};
}
expect<void> client::event_loop(rpc_handler on_rpc, block_pub_handler on_block, txpool_pub_handler on_txpool)
{
MONERO_PRECOND(ctx != nullptr);
assert(signal_sub != nullptr);
std::array<zmq_pollitem_t, 3> poll_items{};
std::size_t count = 0;
poll_items[count++] = {signal_sub.get(), 0, short(ZMQ_POLLIN | ZMQ_POLLERR), 0};
if (daemon != nullptr)
poll_items[count++] = {daemon.get(), 0, short(ZMQ_POLLIN | ZMQ_POLLERR), 0};
if (daemon_sub != nullptr)
poll_items[count++] = {daemon_sub.get(), 0, short(ZMQ_POLLIN | ZMQ_POLLERR), 0};
while (true)
{
const int ready = zmq_poll(poll_items.data(), count, -1);
if (ready < 0)
{
const int err = zmq_errno();
if (err == EINTR)
continue;
MERROR("Failed polling sockets: " << err);
return net::zmq::make_error_code(err);
}
// check for abort messages:
if (poll_items[0].revents & (ZMQ_POLLIN | ZMQ_POLLERR))
{
char buf[1];
MONERO_ZMQ_CHECK(zmq_recv(signal_sub.get(), buf, 1, 0));
switch (buf[0])
{
case 'P':
return {lws::error::signal_abort_process};
case 'S':
return {lws::error::signal_abort_scan};
default:
return {lws::error::signal_unknown};
}
}
// check for rpc responses:
if (daemon && (poll_items[1].revents & (ZMQ_POLLIN | ZMQ_POLLERR)))
{
auto json = net::zmq::receive(daemon.get(), ZMQ_DONTWAIT);
if (!json)
{
if (json != net::zmq::make_error_code(EAGAIN))
{
MERROR("Failed reading RPC response: " << json.error());
return json.error();
}
} else if (on_rpc)
MONERO_CHECK(on_rpc(std::move(*json)));
}
// check for PUB notificatons:
std::size_t sub_index = daemon ? 2 : 1;
if (daemon_sub && (poll_items[sub_index].revents & (ZMQ_POLLIN | ZMQ_POLLERR)))
{
while (true)
{
auto json = net::zmq::receive(daemon_sub.get(), ZMQ_DONTWAIT);
if (!json)
{
if (json == net::zmq::make_error_code(EAGAIN))
break;
MERROR("Failed reading pub message: " << json.error());
return json.error();
}
if (boost::string_ref{*json}.starts_with(minimal_chain_topic))
{
json->erase(0, sizeof(minimal_chain_topic));
auto parsed = rpc::minimal_chain_pub::from_json(std::move(*json));
if (!parsed)
{
MERROR("Failed parsing chain pub: " << parsed.error().message());
return parsed.error();
}
if (on_block)
MONERO_CHECK(on_block(std::move(*parsed)));
}
else if (boost::string_ref{*json}.starts_with(full_txpool_topic))
{
json->erase(0, sizeof(full_txpool_topic));
auto parsed = rpc::full_txpool_pub::from_json(std::move(*json));
if (!parsed)
{
MERROR("Failed parsing txpool pub: " << parsed.error().message());
return parsed.error();
}
if (on_txpool)
MONERO_CHECK(on_txpool(std::move(*parsed)));
}
else
return {lws::error::bad_daemon_response};
}
}
}
return {};
}
expect<account_sub> client::make_account_sub(boost::asio::io_context& io) const
{
MONERO_PRECOND(ctx != nullptr);
net::zmq::socket sub{zmq_socket(ctx->comm.get(), ZMQ_SUB)};
if (!sub)
return {net::zmq::get_error_code()};
MONERO_ZMQ_CHECK(zmq_connect(sub.get(), account_endpoint));
auto client = net::zmq::async_client::make(io, std::move(sub));
if (!client)
return client.error();
return account_sub{ctx, std::move(*client)};
}
expect<net::zmq::async_client> client::make_async_client(boost::asio::io_context& io) const
{
MONERO_PRECOND(ctx != nullptr);
@@ -558,6 +707,114 @@ namespace rpc
return rc;
}
namespace
{
template<typename T>
expect<void> prep_message(epee::byte_stream& sink, const std::string_view prefix, const T& message)
{
sink.write({prefix.data(), prefix.size()});
if (prefix.empty() || prefix.back() != u8':')
sink.push_back(':');
wire::msgpack_slice_writer dest{std::move(sink), true /* integer keys */};
try
{
wire_write::bytes(dest, message);
}
catch (const wire::exception& e)
{
return {e.code()};
}
sink = dest.take_sink();
return success();
}
template<typename T>
expect<epee::byte_slice> prep_message(const std::string_view prefix, const T& message)
{
epee::byte_stream sink;
MONERO_CHECK(prep_message(sink, prefix, message));
return epee::byte_slice{std::move(sink)};
}
}
expect<void> client::push_update(const account& acct, const mempool_receive& src) const
{
if (ctx->internal_pub)
{
auto msg = prep_message(acct.address(), src);
if (!msg)
return msg.error();
const boost::lock_guard<boost::mutex> lock{ctx->sync_internal_pub};
net::zmq::send(std::move(*msg), ctx->internal_pub.get(), ZMQ_DONTWAIT);
}
return success();
}
expect<void> client::push_updates(const epee::span<account> accts, const db::block_id new_height) const
{
MONERO_PRECOND(ctx != nullptr);
if (!ctx->internal_pub)
{
for (account& acct : accts)
acct.updated(new_height);
return success();
}
std::vector<std::size_t> offsets;
offsets.reserve(accts.size());
epee::byte_stream sink;
std::size_t last = 0;
expect<void> rc = success();
for (account& acct : accts)
{
if (new_height < acct.scan_height())
continue;
// make sure `updated` is called on every account even with errors
expect<void> msg = prep_message(sink, acct.address(), acct.updated(new_height));
if (!msg)
rc = msg;
offsets.push_back(sink.size() - last);
last = sink.size();
}
if (!rc)
return rc;
epee::byte_slice msgs{std::move(sink)};
const boost::lock_guard<boost::mutex> lock{ctx->sync_internal_pub};
for (std::size_t offset : offsets)
net::zmq::send(msgs.take_slice(offset), ctx->internal_pub.get(), ZMQ_DONTWAIT);
return success();
}
namespace
{
expect<void> push_warning_(const std::shared_ptr<detail::context>& ctx, const std::error_code error, const db::block_id height)
{
MONERO_PRECOND(ctx != nullptr);
if (ctx->internal_pub)
{
const boost::lock_guard<boost::mutex> lock{ctx->sync_internal_pub};
expect<epee::byte_slice> msg =
prep_message(rpc::feed_warning::prefix(), rpc::feed_warning{error, ctx->warning_count++, height});
if (!msg)
return msg.error();
return net::zmq::send(std::move(*msg), ctx->internal_pub.get(), ZMQ_DONTWAIT);
}
return success();
}
}
expect<void> client::push_warning(const std::error_code error, const db::block_id height) const
{
return push_warning_(ctx, error, height);
}
expect<rates> client::get_rates() const
{
MONERO_PRECOND(ctx != nullptr);
@@ -571,7 +828,7 @@ namespace rpc
return ctx->cached;
}
context context::make(std::string daemon_addr, std::string sub_addr, std::string pub_addr, rmq_details rmq_info, std::chrono::minutes rates_interval, const bool untrusted_daemon)
context context::make(std::string daemon_addr, std::string sub_addr, std::string pub_addr, rmq_details rmq_info, std::chrono::minutes rates_interval, const bool untrusted_daemon, const bool push_updates)
{
zcontext comm{zmq_init(1)};
if (comm == nullptr)
@@ -597,10 +854,20 @@ namespace rpc
if (!rmq_info.address.empty() || !rmq_info.exchange.empty() || !rmq_info.routing.empty() || !rmq_info.credentials.empty())
MONERO_THROW(error::configuration, "RabbitMQ support not enabled");
#endif
net::zmq::socket internal_pub;
if (push_updates)
{
internal_pub.reset(zmq_socket(comm.get(), ZMQ_PUB));
if (internal_pub == nullptr)
MONERO_THROW(net::zmq::get_error_code(), "zmq_socket");
if (zmq_bind(internal_pub.get(), account_endpoint) < 0)
MONERO_THROW(net::zmq::get_error_code(), "zmq_bind");
}
return context{
std::make_shared<detail::context>(
std::move(comm), std::move(pub), std::move(external_pub), rcontext{std::move(rmq_info)}, std::move(daemon_addr), std::move(sub_addr), rates_interval, untrusted_daemon
std::move(comm), std::move(pub), std::move(external_pub), std::move(internal_pub), rcontext{std::move(rmq_info)}, std::move(daemon_addr), std::move(sub_addr), rates_interval, untrusted_daemon
)
};
}
@@ -632,6 +899,11 @@ namespace rpc
return ctx->cache_interval;
}
expect<void> context::push_warning(const std::error_code error) const
{
return push_warning_(ctx, error, db::block_id::txpool);
}
expect<void> context::raise_abort_scan() noexcept
{
MONERO_PRECOND(ctx != nullptr);
+60 -2
View File
@@ -29,6 +29,7 @@
#include <boost/asio/io_context.hpp>
#include <boost/optional/optional.hpp>
#include <chrono>
#include <functional>
#include <memory>
#include <string>
#include <utility>
@@ -38,6 +39,8 @@
#include "db/fwd.h"
#include "common/expect.h" // monero/src
#include "net/zmq.h" // monero/src
#include "net/zmq_async.h"
#include "rpc/fwd.h"
#include "rpc/message.h" // monero/src
#include "rpc/daemon_pub.h"
#include "rpc/rates.h"
@@ -63,6 +66,37 @@ namespace rpc
expect<void> parse_response(cryptonote::rpc::Message& parser, std::string msg, source_location loc = {});
class account_sub
{
std::shared_ptr<const detail::context> ctx;
net::zmq::async_client sub;
public:
account_sub(std::shared_ptr<const detail::context> ctx, net::zmq::async_client sub)
: ctx(std::move(ctx)), sub(std::move(sub))
{}
account_sub(account_sub&&) = default;
account_sub(const account_sub&) = delete;
~account_sub() = default;
account_sub& operator=(account_sub&&) = default;
account_sub& operator=(const account_sub&) = delete;
explicit operator bool() const noexcept { return !sub.close; }
net::zmq::async_client& get() noexcept { return sub; }
//! Subscribe to events from `address` and "warning:"
expect<std::uint32_t> watch(const std::string_view address) const;
//! Initiate complete shutdown on this sub
void shutdown(boost::system::error_code& ec)
{
sub.close = true;
if (sub.asock)
sub.asock->cancel(ec);
}
};
//! Abstraction for ZMQ RPC client. All `const` and `static` methods are thread-safe.
class client
{
@@ -125,12 +159,19 @@ namespace rpc
//! \return True if an external pub/sub was setup
bool has_publish() const noexcept;
//! Reconnect ZMQ connection to daemon
expect<void> daemon_reconnect() noexcept;
//! `wait`, `send`, and `receive` will watch for `raise_abort_scan()`.
expect<void> watch_scan_signals() noexcept;
//! Wait for new block announce or internal timeout.
expect<std::vector<std::pair<topic, std::string>>> wait_for_block();
using rpc_handler = std::function<expect<void>(std::string&&)>;
using block_pub_handler = std::function<expect<void>(minimal_chain_pub&&)>;
using txpool_pub_handler = std::function<expect<void>(full_txpool_pub&&)>;
//! \return A JSON message for RPC request `M`.
template<typename M>
static epee::byte_slice make_message(char const* const name, const M& message)
@@ -138,6 +179,16 @@ namespace rpc
return cryptonote::rpc::FullMessage::getRequest(name, message, 0);
}
//! Loops until an error or shutdown happens, emitting events.
expect<void> event_loop(
rpc_handler on_rpc,
block_pub_handler on_block,
txpool_pub_handler on_txpool
);
//! \return An async ZMQ_SUB socket that is bound to the account update publisher
expect<account_sub> make_account_sub(boost::asio::io_context& io) const;
//! \return `async_client` to daemon. Thread safe.
expect<net::zmq::async_client> make_async_client(boost::asio::io_context& io) const;
@@ -163,6 +214,10 @@ namespace rpc
return publish(epee::byte_slice{std::move(bytes)});
}
expect<void> push_update(const account& acct, const mempool_receive& src) const;
expect<void> push_updates(epee::span<account> accts, db::block_id new_height) const;
expect<void> push_warning(std::error_code error, db::block_id height) const;
//! \return Next available RPC message response from server
expect<std::string> get_message(std::chrono::seconds timeout);
@@ -179,7 +234,7 @@ namespace rpc
\return Recent exchange rates. */
expect<rates> get_rates() const;
};
//! Owns ZMQ context, and ZMQ PUB socket for signalling child `client`s.
class context
{
@@ -205,7 +260,7 @@ namespace rpc
\param True if additional size constraints should be placed on
daemon messages
*/
static context make(std::string daemon_addr, std::string sub_addr, std::string pub_addr, rmq_details rmq_info, std::chrono::minutes rates_interval, const bool untrusted_daemon);
static context make(std::string daemon_addr, std::string sub_addr, std::string pub_addr, rmq_details rmq_info, std::chrono::minutes rates_interval, const bool untrusted_daemon, bool push_updates);
context(context&&) = default;
context(context const&) = delete;
@@ -233,6 +288,9 @@ namespace rpc
return client::make(ctx);
}
//! Push warning to all websocket push clients. Thread safe
expect<void> push_warning(std::error_code error) const;
/*!
All block `client::send`, `client::receive`, and `client::wait` calls
originating from `this` object AND whose `watch_scan_signal` method was
+86
View File
@@ -0,0 +1,86 @@
// Copyright (c) 2026, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <chrono>
#include <cstdint>
#include <memory>
#include <system_error>
#include "db/fwd.h"
#include "rpc/fwd.h"
#include "wire/fwd.h"
namespace lws { namespace rpc { namespace feed
{
using request =
boost::beast::http::request<boost::beast::http::string_body>;
enum class error { none = 0, missed_messages, protocol, queue_max };
// Do not change order or value as these are exported to clients (append)
enum class status : std::uint16_t
{
unspecified_error = 0,
account_not_found,
bad_address,
bad_view_key,
blockchain_reorg,
daemon_unresponsive,
parse_error,
protocol_error,
queue_error,
schema_error
};
WIRE_AS_INTEGER(status);
status map(std::error_code error) noexcept;
const char* get_string(feed::error value) noexcept;
std::error_category const& error_category() noexcept;
inline std::error_code make_error_code(feed::error value) noexcept
{
return std::error_code{int(value), error_category()};
}
bool start(boost::asio::ip::tcp::socket&& sock, boost::asio::io_context& io, const lws::rpc::client& client, std::shared_ptr<const mempool> pool, const request& req, const lws::db::storage& disk, std::chrono::seconds timeout);
bool start(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>&& sock, boost::asio::io_context& io, const lws::rpc::client& client, std::shared_ptr<mempool> pool, const request& req, const lws::db::storage& disk, std::chrono::seconds idle_timeout);
}}} // lws // rpc // feed
namespace std
{
template<>
struct is_error_code_enum<::lws::rpc::feed::error>
: true_type
{};
}
+602
View File
@@ -0,0 +1,602 @@
// Copyright (c) 2026, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/* This file was split into two compilation units `feed_tcp.cpp` and
`feed_ssl.cpp`. This helps reduce the compilation time since each file can be
computed in parallel. The memory usage is also reduced when -j1, which helps
with compilation on smaller machines.
The functions must be in anonymous namespace or marked inline (implicitly or
explicitly) or linker will have issues. Inline is preferred because the
linker will merge the duplicate copies. */
#include "feed.h"
#include <array>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/strand.hpp>
#include <boost/beast/core/error.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/core/string_type.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/write.hpp>
#include <boost/beast/version.hpp>
#include <boost/beast/websocket/stream.hpp>
#include <chrono>
#include <type_traits>
#include <utility>
#include "db/storage.h"
#include "error.h"
#include "misc_log_ex.h"
#include "net/http/slice_body.h"
#include "net/zmq_async.h"
#include "rpc/client.h"
namespace lws { namespace rpc { namespace feed
{
constexpr const std::size_t max_read_size = 2 * 1024; // login call is small
constexpr const std::size_t queue_max = 100; // # messages in queue
//! Suppress scanner messages in this interval when account is catching up
constexpr const std::chrono::seconds scanner_busy{10};
//! Send all block updates if account is within this top blockchain height
constexpr const unsigned on_tip = 10;
constexpr const std::chrono::seconds close_timeout{8}; // for websocket close
constexpr const std::chrono::seconds handshake_timeout{5};
constexpr const std::chrono::seconds login_timeout{8};
constexpr const std::chrono::seconds shutdown_timeout{8}; // for ssl shutdown
constexpr const std::chrono::seconds tx_sync_timeout{20}; // for initial tx sync
enum class event { none = 0, invalid, shutdown, timeout};
enum class protocol : unsigned { invalid = 0, v0_json, v0_msgpack };
inline constexpr const char* get_string(const protocol proto) noexcept
{
switch (proto)
{
case protocol::v0_json:
return "lws.feed.v0.json";
case protocol::v0_msgpack:
return "lws.feed.v0.msgpack";
default:
break;
}
return "unsupported";
}
// defined in `feed_tcp.cpp. Selects preferred protocol
protocol get_protocol(const boost::beast::string_view list) noexcept;
inline bool is_binary(const protocol proto) noexcept
{
switch (proto)
{
case protocol::v0_msgpack:
return true;
default:
break;
}
return false;
}
}}} // lws // rpc // feed
namespace boost
{
namespace system
{
template <>
struct is_error_code_enum<lws::rpc::feed::event>
: std::true_type
{};
} // namespace system
} // namespace boost
namespace lws { namespace rpc { namespace feed
{
// defined in `feed_tcp.cpp
const boost::system::error_category& get_event_category();
inline boost::system::error_code make_error_code(event e)
{
return {static_cast<int>(e), get_event_category()};
}
class connection;
struct connection_sync
{
db::block_id height;
db::block_id last_reported;
std::chrono::steady_clock::time_point last_update;
db::account_id id;
std::uint32_t warnings;
bool on_tip;
bool receives_only;
connection_sync() noexcept
: height(db::block_id::txpool),
last_reported(db::block_id::txpool),
last_update(),
id(db::account_id::invalid),
warnings(-1),
on_tip(false),
receives_only(false)
{}
};
//! Handles initial login, tx sync, and then waits on zmq messages, pushing them to a queue
struct zmq_loop : boost::asio::coroutine
{
std::shared_ptr<connection> self_;
std::size_t iteration_;
explicit zmq_loop(std::shared_ptr<connection> self)
: boost::asio::coroutine(), self_(std::move(self)), iteration_(0)
{}
void operator()(boost::system::error_code error = {}, std::size_t = {});
};
// defined in `feed_tcp.cpp`
std::string_view get_prefix(const boost::beast::net::const_buffer buf);
expect<epee::byte_slice> prep_error(std::error_code error, protocol proto);
expect<epee::byte_slice> prep_login(const db::storage& disk, const mempool* pool, account_sub* sub, connection_sync* sync, boost::beast::flat_buffer& buffer, protocol proto);
expect<epee::byte_slice> prep_update(const db::storage& disk, std::string&& source, connection_sync* sync, protocol proto);
class connection
{
lws::db::storage disk_;
const std::shared_ptr<const mempool> pool_;
std::string sub_buffer_;
boost::beast::flat_buffer ws_buffer_;
std::deque<epee::byte_slice> write_queue_;
account_sub sub_;
boost::asio::io_context::strand strand_;
boost::asio::steady_timer socket_timer_;
std::size_t pull_iter_;
connection_sync sync_;
const protocol proto_;
virtual void async_shutdown(std::shared_ptr<connection> self) = 0;
protected:
template<typename T>
void do_async_close(T& sock, std::shared_ptr<connection> self)
{
LWS_VERIFY(self);
MDEBUG("Starting websocket close on " << this);
boost::system::error_code ec{};
sub_.shutdown(ec);
start_timeout(close_timeout, self);
sock.async_close(boost::beast::websocket::close_reason(), bind(std::bind(&connection::async_shutdown, self, self)));
}
void do_async_shutdown(boost::asio::ip::tcp::socket& sock, std::shared_ptr<connection>)
{
do_cleanup(sock);
}
void do_async_shutdown(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>& sock, std::shared_ptr<connection> self)
{
LWS_VERIFY(self);
MDEBUG("Starting TLS/SSL shutdown on " << this);
boost::system::error_code ec{};
sub_.shutdown(ec);
start_timeout(shutdown_timeout, self);
sock.async_shutdown(bind(std::bind(&connection::cleanup, self)));
}
void do_cleanup(boost::asio::ip::tcp::socket& sock)
{
MDEBUG("Websocket cleanup on " << this);
boost::system::error_code ec{};
sub_.shutdown(ec);
socket_timer_.cancel();
sock.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
sock.close(ec);
}
void do_cleanup(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>& sock)
{
do_cleanup(sock.next_layer());
}
template<typename T>
void do_stop_reads(T& sock, std::shared_ptr<connection> self)
{
LWS_VERIFY(self);
ws_buffer_.shrink_to_fit();
sock.async_read(ws_buffer_, bind(std::bind(&connection::close_check, self, self, event::invalid)));
}
template<typename T>
void do_async_read(T& sock, boost::asio::steady_timer::duration timeout, zmq_loop callback)
{
if (is_closing())
return callback(event::shutdown);
LWS_VERIFY(callback.self_);
start_timeout(timeout, callback);
sock.async_read(ws_buffer_, bind(std::move(callback)));
}
template<typename T>
static void do_async_write_queue(T& sock, std::shared_ptr<connection> self, epee::byte_slice&& first)
{
// do not check `is_closing()`. Instead, try to write out all buffered data
LWS_VERIFY(self && self->write_queue_.empty());
struct push_loop
{
T* sock_;
std::shared_ptr<connection> self_;
void async_write()
{
LWS_VERIFY(sock_ && self_ && !self_->write_queue_.empty());
connection& self = *self_;
const epee::byte_slice& next = self.write_queue_.front();
MDEBUG("Sending websocket " << get_prefix({next.data(), next.size()}) << " message on " << self_.get());
sock_->async_write(
boost::asio::buffer(next.data(), next.size()), self.bind(std::move(*this))
);
}
void operator()(const boost::system::error_code error, std::size_t)
{
LWS_VERIFY(self_ && !self_->write_queue_.empty());
connection& self = *self_;
if (error)
{
MDEBUG("Write error on websocket (" << self_.get() << "): " << error.message());
return self.async_close(std::move(self_));
}
self.write_queue_.pop_front();
if (!self.write_queue_.empty())
async_write();
else if (self.is_closing())
return self.async_close(std::move(self_));
}
};
self->write_queue_.push_back(std::move(first));
push_loop{std::addressof(sock), std::move(self)}.async_write();
}
bool do_write(std::shared_ptr<connection> self, expect<epee::byte_slice>&& message)
{
const bool rc = message.has_value();
if (!rc)
{
MDEBUG("Error in websocket (" << this << "): " << message.error().message());
message = MONERO_UNWRAP(prep_error(message.error(), proto_));
}
else if (message->empty())
return true; // nop update (scanner is _very_ active)
if (write_queue_.empty())
async_write_queue(std::move(self), std::move(*message));
else
write_queue_.push_back(std::move(*message));
return rc;
}
static bool sync_iter(std::size_t& ours, const std::size_t theirs) noexcept
{
if (ours != theirs)
return false;
++ours;
return true;
}
public:
connection(boost::asio::io_context& io, const lws::rpc::client& client, std::shared_ptr<const mempool> pool, const lws::db::storage& disk, const protocol proto)
: disk_(disk.clone()),
pool_(std::move(pool)),
sub_buffer_(),
ws_buffer_(),
write_queue_(),
sub_(MONERO_UNWRAP(client.make_account_sub(io))),
strand_(io),
socket_timer_(io),
pull_iter_(0),
sync_{},
proto_(proto)
{
ws_buffer_.max_size(max_read_size);
}
virtual ~connection() noexcept { MDEBUG("Destroying websocket on " << this); }
protocol proto() const noexcept { return proto_; }
bool is_closing() const noexcept { return !bool(sub_); }
bool sync_pull(const std::size_t iter) noexcept { return sync_iter(pull_iter_, iter); }
bool login(std::shared_ptr<connection> self)
{
const bool rc = do_write(std::move(self), prep_login(disk_, pool_.get(), &sub_, &sync_, ws_buffer_, proto_));
ws_buffer_.consume(ws_buffer_.cdata().size());
return rc;
}
// Push message from ZMQ to websocket (maybe)
bool push(std::shared_ptr<connection> self)
{
bool rc = false;
if (queue_max < write_queue_.size())
{
MDEBUG("Websocket max queue reached on " << this);
write_queue_.push_back(MONERO_UNWRAP(prep_error({error::queue_max}, proto_)));
}
else
{
MDEBUG("Websocket queued event from scanner on " << this);
rc = do_write(std::move(self), prep_update(disk_, std::move(sub_buffer_), &sync_, proto_));
}
return rc;
}
template<typename F>
auto bind(F&& f) -> decltype(boost::asio::bind_executor(strand_, std::forward<F>(f)))
{
return boost::asio::bind_executor(strand_, std::forward<F>(f));
}
void client_close(const boost::system::error_code error = {})
{
if (error)
MDEBUG("Preparing websocket close (" << this << "): " << error.message());
else
MDEBUG("Preparing websocket close on " << this);
boost::system::error_code ec{};
socket_timer_.cancel();
sub_.shutdown(ec);
// writes may need to be flushed, so don't cancel socket ops
}
void close_check(std::shared_ptr<connection> self, const boost::system::error_code error = {})
{
if (write_queue_.empty())
async_close(std::move(self));
else
client_close(error);
}
void start_timeout(boost::asio::steady_timer::duration timeout, std::shared_ptr<connection> self)
{
struct force_close
{
std::shared_ptr<connection> self_;
void operator()(const boost::system::error_code error) const
{
if (error != boost::asio::error::operation_aborted)
{
LWS_VERIFY(self_);
MDEBUG("Websocket timeout on " << self_.get());
self_->cleanup();
}
}
};
LWS_VERIFY(self);
socket_timer_.expires_after(timeout);
socket_timer_.async_wait(bind(force_close{std::move(self)}));
}
void start_timeout(boost::asio::steady_timer::duration timeout, zmq_loop handler)
{
struct notify
{
zmq_loop f_;
void operator()(const boost::system::error_code error)
{
if (!error)
f_(event::timeout);
else if (error != boost::asio::error::operation_aborted)
f_(error);
// else another timer was queued via expires_after
}
};
LWS_VERIFY(handler.self_);
socket_timer_.expires_after(timeout);
socket_timer_.async_wait(bind(notify{std::move(handler)}));
}
void async_zmq_wait(zmq_loop callback)
{
if (is_closing())
return callback(event::shutdown);
// Skip timeout
LWS_VERIFY(callback.self_);
net::zmq::async_read(sub_.get(), sub_buffer_, bind(std::move(callback)));
}
//! Start socket close routine. `loop::` must be exited after this.
virtual void async_close(std::shared_ptr<connection> self) = 0;
virtual void cleanup() = 0;
virtual void stop_reads(std::shared_ptr<connection> self) = 0;
virtual void async_read(boost::asio::steady_timer::duration timeout, zmq_loop callback) = 0;
virtual void async_write_queue(std::shared_ptr<connection> self, epee::byte_slice&& msg) = 0;
};
template<typename T>
class connection_ final : public connection
{
boost::beast::websocket::stream<T, false> sock_;
virtual void async_shutdown(std::shared_ptr<connection> self) override final
{ do_async_shutdown(sock().next_layer(), std::move(self)); }
public:
explicit connection_(T&& sock, boost::asio::io_context& io, const lws::rpc::client& client, std::shared_ptr<const mempool> pool, const lws::db::storage& disk, const protocol proto)
: connection(io, client, std::move(pool), disk, proto), sock_(std::move(sock))
{
if (is_binary(proto))
sock_.binary(true);
}
boost::beast::websocket::stream<T, false>& sock() { return sock_; }
virtual void async_close(std::shared_ptr<connection> self) override final
{ do_async_close(sock(), std::move(self)); }
virtual void cleanup() override final { do_cleanup(sock().next_layer()); }
virtual void stop_reads(std::shared_ptr<connection> self) override final
{ do_stop_reads(sock(), std::move(self)); }
virtual void async_read(boost::asio::steady_timer::duration timeout, zmq_loop callback) override final
{ do_async_read(sock(), timeout, std::move(callback)); }
virtual void async_write_queue(std::shared_ptr<connection> self, epee::byte_slice&& msg) override final
{ do_async_write_queue(sock(), std::move(self), std::move(msg)); }
};
inline void zmq_loop::operator()(const boost::system::error_code error, const std::size_t bytes)
{
LWS_VERIFY(self_);
connection& self = *self_;
if (!self.sync_pull(iteration_)) // sync all copies of loop
return;
++iteration_;
BOOST_ASIO_CORO_REENTER(*this)
{
if (!error) // handshake
{
BOOST_ASIO_CORO_YIELD self.async_read(login_timeout, std::move(*this));
if (!error)
{
self.stop_reads(self_); // client can only send control frames now
if (!self.login(self_)) // always queues a write
return self.client_close();
}
}
if (error)
{
MDEBUG("Websocket failure (" << self_.get() << "): " << error.message());
return self.async_close(std::move(self_)); // login never queued write
}
while (true)
{
BOOST_ASIO_CORO_YIELD self.async_zmq_wait(std::move(*this));
if (error || !self.push(self_))
return self.close_check(self_, error);
}
}
}
inline const boost::asio::ip::tcp::socket& get_tcp_sock(const boost::asio::ip::tcp::socket& sock) noexcept { return sock; }
inline const boost::asio::ip::tcp::socket& get_tcp_sock(const boost::asio::ssl::stream<boost::asio::ip::tcp::socket>& sock) { return sock.next_layer(); }
template<typename T>
inline void do_start(std::shared_ptr<connection_<T>> self, const request& req, const std::chrono::seconds timeout)
{
struct on_control
{
std::weak_ptr<connection> self_;
void operator()(boost::beast::websocket::frame_type type, boost::beast::string_view)
{
switch (type)
{
default:
case boost::beast::websocket::frame_type::close:
{
const auto self = self_.lock();
if (!self)
return;
MDEBUG("Websocket client requested close on " << self.get());
self->close_check(self);
break;
}
case boost::beast::websocket::frame_type::ping:
case boost::beast::websocket::frame_type::pong:
MDEBUG("Received control frame (ping/pong) from " << self_.lock().get());
break;
}
}
};
LWS_VERIFY(self);
connection_<T>& obj = *self;
boost::system::error_code ec{};
MDEBUG("Starting websocket handshake on " << get_tcp_sock(self->sock().next_layer()).remote_endpoint(ec) << " / " << self.get());
obj.sock().control_callback(on_control{self});
{
namespace http = boost::beast::http;
using stream = boost::beast::websocket::stream_base;
obj.sock().set_option(stream::timeout{handshake_timeout, timeout, true /* pings */});
const protocol proto = obj.proto();
obj.sock().set_option(
stream::decorator(
[proto] (http::response_header<>& hdr)
{ hdr.set(http::field::sec_websocket_protocol, get_string(proto)); }
)
);
}
obj.sock().async_accept(req, obj.bind(zmq_loop{std::move(self)}));
}
template<typename T>
inline bool do_start(T&& sock, boost::asio::io_context& io, const lws::rpc::client& client, std::shared_ptr<const mempool> pool, const request& req, const lws::db::storage& disk, const std::chrono::seconds timeout)
{
const protocol proto = get_protocol(req.base()[boost::beast::http::field::sec_websocket_protocol]);
switch (proto)
{
case protocol::v0_msgpack:
case protocol::v0_json:
do_start(std::make_shared<connection_<T>>(std::forward<T>(sock), io, client, std::move(pool), disk, proto), req, timeout);
return true;
default:
case protocol::invalid:
break;
}
return false;
}
}}} // lws // rpc // feed
+44
View File
@@ -0,0 +1,44 @@
// Copyright (c) 2026, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <boost/asio/ssl.hpp>
#include <boost/beast/websocket/ssl.hpp>
#include "feed.inl"
namespace lws { namespace rpc { namespace feed
{
bool start(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>&& sock, boost::asio::io_context& io, const lws::rpc::client& client, std::shared_ptr<const mempool> pool, const request& req, const lws::db::storage& disk, const std::chrono::seconds timeout)
{
// moving ssl streams only supported in boost 1.74+
#if BOOST_VERSION >= 107400
return do_start(std::move(sock), io, client, std::move(pool), req, disk, timeout);
#else
return false;
#endif
}
}}}
+374
View File
@@ -0,0 +1,374 @@
// Copyright (c) 2026, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "feed.inl"
#include <cstring>
#include "db/account.h"
#include "db/string.h"
#include "error.h"
#include "rpc/light_wallet.h"
#include "rpc/login.h"
#include "wire/error.h"
#include "wire/json.h"
#include "wire/msgpack.h"
namespace lws { namespace rpc { namespace feed
{
status map(const std::error_code error) noexcept
{
if (error == lws::error::blockchain_reorg)
return status::blockchain_reorg;
if (error == lws::error::daemon_timeout)
return status::daemon_unresponsive;
if (error == error::queue_max)
return status::queue_error;
if (error == error::missed_messages)
return status::queue_error;
if (error == lws::error::bad_address)
return status::bad_address;
if (error == lws::error::bad_view_key)
return status::bad_view_key;
if (error == lws::error::account_not_found)
return status::account_not_found;
if (error == error::protocol)
return status::protocol_error;
const std::error_category& category = error.category();
if (category == wire::error::schema_category())
return status::schema_error;
if (category == wire::error::msgpack_category())
return status::parse_error;
if (category == wire::error::rapidjson_category())
return status::parse_error;
return status::unspecified_error;
}
const char* get_string(error value) noexcept
{
switch(value)
{
case error::missed_messages:
return "Internal client thread missed internal ZeroMQ messages";
case error::protocol:
return "Client did not follow protocool";
case error::queue_max:
return "Max queue for outgoing push messages was reached";
default:
break;
}
return "unknown lws::rpc::feed::error";
}
namespace
{
class event_category : public boost::system::error_category
{
public:
// Return a short descriptive name for the category
virtual const char *name() const noexcept override final { return "lws::rpc::push::event"; }
// Return what each enum means in text
virtual std::string message(int c) const override final
{
switch(event(c))
{
case event::none:
return "No error";
case event::invalid:
return "Client did not follow protocol";
case event::timeout:
return "Timeout on async operation";
default:
break;
}
return "unknown";
}
virtual boost::system::error_condition default_error_condition(int c) const noexcept override final
{
switch(event(c))
{
case event::timeout:
return make_error_condition(boost::system::errc::timed_out);
default:
break;
}
return boost::system::error_condition(c, *this);
}
};
struct category final : std::error_category
{
virtual const char* name() const noexcept override final
{
return "lws::rpc::push:error_category()";
}
virtual std::string message(int value) const override final
{
return get_string(lws::rpc::feed::error(value));
}
virtual std::error_condition default_error_condition(int value) const noexcept override final
{
switch (lws::rpc::feed::error(value))
{
case error::protocol:
return std::errc::protocol_error;
case error::queue_max:
return std::errc::no_buffer_space;
default:
break; // map to unmatchable category
}
return std::error_condition{value, *this};
}
};
} // anonymous
const boost::system::error_category& get_event_category()
{
static event_category c;
return c;
}
std::error_category const& error_category() noexcept
{
static const category instance{};
return instance;
}
namespace
{
constexpr std::pair<boost::beast::string_view, protocol> get_map(const protocol proto) noexcept
{
return {get_string(proto), proto};
}
std::string get_string(const boost::beast::net::const_buffer buf)
{
return {reinterpret_cast<const char*>(buf.data()), buf.size()};
}
epee::byte_slice get_slice(const boost::beast::net::const_buffer buf)
{
return epee::byte_slice{{reinterpret_cast<const std::uint8_t*>(buf.data()), buf.size()}};
}
template<typename T>
expect<epee::byte_slice> prep(const T& src, const protocol proto)
{
epee::byte_stream sink{};
const std::string_view prefix = T::prefix();
sink.write({prefix.data(), prefix.size()});
const std::error_code error = proto == protocol::v0_msgpack ?
wire::msgpack::to_bytes(sink, src) : wire::json::to_bytes(sink, src);
if (error)
return error;
return epee::byte_slice{std::move(sink)};
}
} // anonymous
protocol get_protocol(const boost::beast::string_view list) noexcept
{
// in order of preference
static constexpr const std::array<std::pair<boost::beast::string_view, protocol>, 2> supported
{{
get_map(protocol::v0_msgpack),
get_map(protocol::v0_json)
}};
const boost::beast::http::token_list tokens{list};
for (const auto token : tokens)
{
for (const auto support : supported)
{
if (support.first == token)
return support.second;
}
}
return protocol::invalid;
}
std::string_view get_prefix(const boost::beast::net::const_buffer buf)
{
char const* const begin = reinterpret_cast<const char*>(buf.data());
void const* const match = std::memchr(begin, u8':', buf.size());
if (match)
return {begin, std::size_t(reinterpret_cast<const char*>(match) - begin + 1)};
return {};
}
expect<epee::byte_slice> prep_error(const std::error_code error, const protocol proto)
{
return prep(feed_error{error}, proto);
}
expect<epee::byte_slice> prep_login(const db::storage& disk, mempool const* const pool, account_sub* const sub, connection_sync* const sync, boost::beast::flat_buffer& buffer, const protocol proto)
{
LWS_VERIFY(sub && sync);
const std::string_view prefix = get_prefix(buffer.cdata());
if (prefix != feed_login::prefix())
return {error::protocol};
buffer.consume(prefix.size());
feed_login login{};
std::error_code error = proto == protocol::v0_msgpack ?
wire::msgpack::from_bytes(get_slice(buffer.cdata()), login) :
wire::json::from_bytes(get_string(buffer.cdata()), login);
if (error)
return error;
/* Must sub to feed before starting db read or else lost txes could
occur. If the client gets txes twice, it should merge via hash. */
const expect<std::uint32_t> watcher = sub->watch(db::address_string(login.account.address));
if (!watcher)
return watcher.error();
auto account = open_account(login.account, disk);
if (!account)
return account.error();
sync->receives_only = login.receives_only;
sync->id = account->first.id;
sync->warnings = *watcher;
if (!login.tx_sync)
{
const auto block = account->second.get_last_block();
if (!block)
return block.error();
account->second.finish_read();
const db::account& acct = account->first;
return prep(feed_blocks{acct.start_height, acct.scan_height, block->id, acct.lookahead_fail, acct.lookahead}, proto);
}
auto txs = get_address_txs_response::load(account->second, account->first, pool, true);
if (!txs)
return txs.error();
account->second.finish_read();
return prep(feed_tx_sync{std::move(*txs)}, proto);
}
//! Convert ZMQ_PUSH message from the scanner to websocket `update` message
expect<epee::byte_slice> prep_update(const db::storage& disk, std::string&& source, connection_sync* const sync, const protocol proto)
{
LWS_VERIFY(sync);
epee::byte_slice slice{std::move(source)};
const std::string_view prefix = get_prefix({slice.data(), slice.size()});
const bool warning = prefix == feed_warning::prefix();
slice.remove_prefix(prefix.size());
if (warning)
{
if (sync->receives_only)
return epee::byte_slice{};
feed_warning the_warning{};
const std::error_code error = wire::msgpack::from_bytes(std::move(slice), the_warning);
if (!error)
return error;
if (sync->warnings != the_warning.counter)
return prep_error(error::missed_messages, proto);
++sync->warnings;
if (the_warning.code == status::blockchain_reorg && sync->height != db::block_id::txpool)
sync->height = std::min(sync->height, the_warning.height);
return prep(the_warning, proto);
}
account::update from_scanner{};
const std::error_code error = wire::msgpack::from_bytes(std::move(slice), from_scanner);
if (error)
return error;
if (from_scanner.new_height != db::block_id::txpool)
{
if (sync->height != db::block_id::txpool && sync->height != from_scanner.old_height)
return prep_error(error::missed_messages, proto);
sync->height = from_scanner.new_height;
if (sync->receives_only && from_scanner.outputs.empty())
return epee::byte_slice{};
// suppress _some_ block messages when account is catching up
if (!sync->on_tip && from_scanner.outputs.empty() && from_scanner.spends.empty() && std::chrono::steady_clock::now() - sync->last_update <= scanner_busy)
return epee::byte_slice{};
feed_blocks blocks{
std::min(sync->last_reported, from_scanner.old_height),
from_scanner.new_height,
from_scanner.new_height
};
/* A read is unfortunate here, but it also catches accounts that have
been moved to "hidden". The stream will immediately abort on error. */
auto reader = disk.start_read();
if (!reader)
return reader.error();
const auto last_block = reader->get_last_block();
if (!last_block)
return last_block.error();
auto account = reader->get_account(db::account_status::active, sync->id);
if (account == lws::error::account_not_found)
account = reader->get_account(db::account_status::inactive, sync->id); // rare as scanner should not push updates
if (!account)
return account.error();
blocks.blockchain_height = last_block->id;
blocks.lookahead_fail = account->lookahead_fail;
blocks.lookahead = account->lookahead;
reader->finish_read();
blocks.transactions =
get_address_txs_response::load(std::move(from_scanner.outputs), std::move(from_scanner.spends));
sync->on_tip = (to_uint(last_block->id) - to_uint(std::min(last_block->id, from_scanner.new_height))) <= on_tip;
sync->last_update = std::chrono::steady_clock::now();
sync->last_reported = from_scanner.new_height;
return prep(blocks, proto);
}
// mempool
LWS_VERIFY(from_scanner.outputs.size() == 1);
return prep(feed_mempool{from_scanner.outputs.at(0)}, proto);
}
bool start(boost::asio::ip::tcp::socket&& sock, boost::asio::io_context& io, const lws::rpc::client& client, std::shared_ptr<const mempool> pool, const request& req, const lws::db::storage& disk, const std::chrono::seconds timeout)
{
return do_start(std::move(sock), io, client, std::move(pool), req, disk, timeout);
}
}}} // lws // rpc // feed
+10
View File
@@ -27,12 +27,22 @@
#pragma once
#include <cstdint>
namespace lws
{
namespace rpc
{
struct account_credentials;
class client;
namespace feed
{
enum class error;
enum class status : std::uint16_t;
}
}
class mempool;
struct rates;
class scan_manager;
}
+498 -6
View File
@@ -33,13 +33,17 @@
#include <limits>
#include <stdexcept>
#include <type_traits>
#include <unordered_set>
#include "config.h"
#include "db/storage.h"
#include "db/string.h"
#include "error.h"
#include "lws_version.h"
#include "mempool.h"
#include "time_helper.h" // monero/contrib/epee/include
#include "ringct/rctOps.h" // monero/src
#include "rpc/feed.h"
#include "span.h" // monero/contrib/epee/include
#include "util/random_outputs.h"
#include "version.h" // monero/src
@@ -47,6 +51,7 @@
#include "wire/adapted/crypto.h"
#include "wire/error.h"
#include "wire/json.h"
#include "wire/msgpack/read.h"
#include "wire/traits.h"
#include "wire/vector.h"
#include "wire/wrapper/array.h"
@@ -196,7 +201,7 @@ namespace lws
source.end_array();
}
void rpc::read_bytes(wire::json_reader& source, account_credentials& self)
void rpc::read_bytes(wire::reader& source, account_credentials& self)
{
std::string address;
wire::object(source,
@@ -237,6 +242,217 @@ namespace lws
);
}
rpc::feed_error::feed_error(std::error_code error)
: msg(error.message()),
code(feed::map(error))
{}
void rpc::write_bytes(wire::writer& dest, const feed_error& self)
{
wire::object(dest, WIRE_FIELD(msg), WIRE_FIELD(code));
}
void rpc::read_bytes(wire::reader& src, feed_login& self)
{
wire::object(src,
WIRE_FIELD(account),
WIRE_FIELD_DEFAULTED(tx_sync, true),
WIRE_FIELD_DEFAULTED(receives_only, false)
);
}
void rpc::write_bytes(wire::writer& dest, const feed_mempool& self)
{
epee::span<const std::uint8_t> const* payment_id = nullptr;
epee::span<const std::uint8_t> payment_id_bytes;
const auto extra = db::unpack(self.received.extra);
if (extra.second)
{
payment_id = std::addressof(payment_id_bytes);
if (extra.second == sizeof(self.received.payment_id.short_))
payment_id_bytes = epee::as_byte_span(self.received.payment_id.short_);
else
payment_id_bytes = epee::as_byte_span(self.received.payment_id.long_);
}
rct::key const* optional_rct = nullptr;
if (extra.first & lws::db::ringct_output)
optional_rct = std::addressof(self.received.ringct_mask);
wire::object(dest,
wire::field("hash", std::cref(self.received.link.tx_hash)),
wire::field("prefix_hash", std::cref(self.received.tx_prefix_hash)),
wire::field("fee", self.received.fee),
wire::field("unlock_time", self.received.unlock_time),
wire::optional_field("payment_id", payment_id),
wire::field("mixin", self.received.spend_meta.mixin_count),
wire::field("amount", self.received.spend_meta.amount),
wire::field("public_key", std::cref(self.received.pub)),
wire::field("tx_pub_key", std::cref(self.received.spend_meta.tx_public)),
wire::field("index", self.received.spend_meta.index),
wire::optional_field("recipient", wire::defaulted(std::cref(self.received.recipient), db::address_index{})),
wire::optional_field("rct", optional_rct)
);
}
namespace
{
template<typename T>
struct sync_wrapper
{
const T& data;
};
struct sync_transform_
{
template<typename T>
sync_wrapper<T> operator()(const T& value) const noexcept
{ return {value}; }
};
constexpr const sync_transform_ sync_transform{};
struct legacy_temp { db::output_id id; };
struct composite_temp { db::output_id id; };
bool operator!=(const composite_temp& left, const composite_temp& right) noexcept
{ return left.id != right.id; }
void write_bytes(wire::writer& dest, const legacy_temp& self)
{
wire::object(dest,
wire::field("amount", self.id.high),
wire::field("index", self.id.low)
);
}
void write_bytes(wire::writer& dest, const composite_temp& self)
{
wire::object(dest, wire::field("legacy", legacy_temp{self.id}));
}
void write_bytes(wire::writer& dest, sync_wrapper<db::output> self)
{
rct::key rct{};
rct::key const* optional_rct = nullptr;
const auto flags = unpack(self.data.extra).first;
if (flags & lws::db::ringct_output)
{
if (!(flags & lws::db::coinbase_output))
rct = self.data.ringct_mask;
else
rct = rct::identity();
optional_rct = std::addressof(rct);
}
wire::object(dest,
wire::field("amount", self.data.spend_meta.amount),
wire::field("public_key", std::cref(self.data.pub)),
wire::field("index", self.data.spend_meta.index),
wire::optional_field("id", wire::defaulted(composite_temp{self.data.spend_meta.id}, composite_temp{db::output_id::txpool()})),
wire::field("tx_pub_key", std::cref(self.data.spend_meta.tx_public)),
wire::optional_field("rct", optional_rct),
wire::optional_field("recipient", wire::defaulted(std::cref(self.data.recipient), db::address_index{}))
);
}
void write_bytes(wire::writer& dest, sync_wrapper<rpc::transaction_spend> self)
{
wire::object(dest,
wire::field("id", composite_temp{self.data.possible_spend.source}),
wire::field("key_image", std::cref(self.data.possible_spend.image))
);
}
void write_bytes(wire::writer& dest, sync_wrapper<rpc::get_transaction> self)
{
epee::span<const std::uint8_t> const* payment_id = nullptr;
epee::span<const std::uint8_t> payment_id_bytes;
const auto extra = db::unpack(self.data.info.extra);
if (extra.second)
{
payment_id = std::addressof(payment_id_bytes);
if (extra.second == sizeof(self.data.info.payment_id.short_))
payment_id_bytes = epee::as_byte_span(self.data.info.payment_id.short_);
else
payment_id_bytes = epee::as_byte_span(self.data.info.payment_id.long_);
}
const bool is_coinbase = (extra.first & db::coinbase_output);
const bool txpool = self.data.info.link.height == db::block_id::txpool;
wire::object(dest,
wire::field("hash", std::cref(self.data.info.link.tx_hash)),
wire::field("prefix_hash", std::cref(self.data.info.tx_prefix_hash)),
wire::field("timestamp", self.data.info.timestamp),
wire::field("fee", self.data.info.fee),
wire::field("unlock_time", self.data.info.unlock_time),
wire::optional_field("height", wire::defaulted(self.data.info.link.height, db::block_id::txpool)),
wire::optional_field("payment_id", payment_id),
wire::optional_field("coinbase", wire::defaulted(is_coinbase, false)),
wire::optional_field("mempool", wire::defaulted(txpool, false)),
wire::field("mixin", self.data.info.spend_meta.mixin_count),
wire::optional_field("spends", wire::array(boost::adaptors::transform(self.data.spends, sync_transform))),
wire::optional_field("receives", wire::array(boost::adaptors::transform(self.data.receives, sync_transform)))
);
}
}
void rpc::write_bytes(wire::writer& dest, const feed_tx_sync& self)
{
wire::object(dest,
wire::field("scanned_block_height", self.info.scanned_block_height),
wire::field("start_height", self.info.start_height),
wire::field("blockchain_height", self.info.blockchain_height),
wire::optional_field("lookahead_fail", wire::defaulted(self.info.lookahead_fail, unsigned(0))),
wire::optional_field("lookahead", wire::defaulted(self.info.lookahead, db::address_index{})),
wire::optional_field("transactions", wire::array(boost::adaptors::transform(self.info.transactions, sync_transform)))
);
}
void rpc::write_bytes(wire::writer& dest, const feed_blocks& self)
{
wire::object(dest,
WIRE_FIELD(scan_start),
WIRE_FIELD(scan_end),
WIRE_FIELD(blockchain_height),
wire::optional_field("lookahead_fail", wire::defaulted(self.lookahead_fail, db::block_id(0))),
wire::optional_field("lookahead", wire::defaulted(std::cref(self.lookahead), db::address_index{})),
wire::optional_field("transactions", wire::array(boost::adaptors::transform(self.transactions, sync_transform)))
);
}
rpc::feed_warning::feed_warning(std::error_code error, const std::uint32_t counter, const db::block_id height)
: msg(error.message()),
code(feed::map(error)),
counter(counter),
height(height)
{}
namespace
{
template<typename F, typename T>
void map_feed_warning(F& format, T& self)
{
wire::object(format,
WIRE_FIELD_ID(0, msg),
WIRE_FIELD_ID(1, code),
WIRE_FIELD_ID(2, counter),
WIRE_FIELD_ID(3, height)
);
}
}
void rpc::read_bytes(wire::msgpack_reader& src, feed_warning& dest)
{ map_feed_warning(src, dest); }
void rpc::write_bytes(wire::writer& dest, const feed_warning& src)
{ map_feed_warning(dest, src); }
void rpc::write_bytes(wire::json_writer& dest, const new_subaddrs_response& self)
{
wire::object(dest, WIRE_FIELD(new_subaddrs), WIRE_FIELD(all_subaddrs));
@@ -275,7 +491,7 @@ namespace lws
namespace rpc
{
static void write_bytes(wire::json_writer& dest, boost::range::index_value<const get_address_txs_response::transaction&> self)
static void write_bytes(wire::json_writer& dest, boost::range::index_value<const get_transaction&> self)
{
epee::span<const std::uint8_t> const* payment_id = nullptr;
epee::span<const std::uint8_t> payment_id_bytes;
@@ -292,25 +508,301 @@ namespace lws
}
const bool is_coinbase = (extra.first & db::coinbase_output);
const auto height = self.value().info.link.height;
const bool mempool = height == db::block_id::txpool;
const iso_timestamp timestamp{self.value().info.timestamp};
wire::object(dest,
wire::field("id", std::uint64_t(self.index())),
wire::field("hash", std::cref(self.value().info.link.tx_hash)),
wire::field("timestamp", iso_timestamp(self.value().info.timestamp)),
wire::optional_field("timestamp", mempool ? nullptr : &timestamp),
wire::field("total_received", safe_uint64(self.value().info.spend_meta.amount)),
wire::field("total_sent", safe_uint64(self.value().spent)),
wire::field("fee", safe_uint64(self.value().info.fee)),
wire::field("unlock_time", self.value().info.unlock_time),
wire::field("height", self.value().info.link.height),
wire::optional_field("height", mempool ? nullptr : &height),
wire::optional_field("payment_id", payment_id),
wire::field("coinbase", is_coinbase),
wire::field("mempool", false),
wire::field("mempool", mempool),
wire::field("mixin", self.value().info.spend_meta.mixin_count),
wire::field("recipient", self.value().info.recipient),
wire::field("spent_outputs", std::cref(self.value().spends))
);
}
} // rpc
std::vector<db::output::spend_meta_>::const_iterator
rpc::get_address_txs_response::find_metadata(std::vector<db::output::spend_meta_> const& metas, const db::output_id id)
{
struct by_output_id
{
bool operator()(db::output::spend_meta_ const& left, db::output_id right) const noexcept
{
return left.id < right;
}
bool operator()(db::output_id left, db::output::spend_meta_ const& right) const noexcept
{
return left < right.id;
}
};
return std::lower_bound(metas.begin(), metas.end(), id, by_output_id{});
}
namespace
{
template<typename T>
struct vec_lmdb_
{
using iterator = typename std::vector<T>::const_iterator;
using value_type = T;
iterator pos;
const iterator end;
bool is_end() const { return pos == end; }
vec_lmdb_& operator++()
{
++pos;
return *this;
}
template<typename U, typename G = U, std::size_t uoffset = 0>
G get_value() const noexcept
{
static_assert(std::is_same<U, T>(), "bad MONERO_FIELD usage?");
static_assert(std::is_trivially_copyable<U>(), "value type must be memcpy safe");
static_assert(std::is_trivially_copyable<G>(), "field type must be memcpy safe");
static_assert(sizeof(G) + uoffset <= sizeof(U), "bad field and/or offset");
assert(sizeof(G) + uoffset <= end - pos);
assert(!is_end());
G value;
std::memcpy(std::addressof(value), reinterpret_cast<const std::uint8_t*>(std::addressof(*pos)) + uoffset, sizeof(value));
return value;
}
const value_type& operator*() const noexcept { return *pos; }
};
template<typename T>
vec_lmdb_<T> vec_lmdb(const std::vector<T>& src) { return {src.begin(), src.end()}; }
template<typename T, typename U>
std::pair<std::vector<rpc::get_transaction>, std::uint64_t> merge_into_txes(T output, U spend, db::account const* const user, mempool const* const pool, const std::size_t reserve, const bool all_outputs, const bool skip_spend_meta)
{
// merge input and output info into a single set of txes.
const bool has_pool = (user && pool);
std::uint64_t total = 0;
std::vector<rpc::get_transaction> out{};
std::vector<db::output::spend_meta_> metas{};
std::vector<std::pair<db::output_id, db::address_index>> receives{};
std::unordered_set<crypto::hash> txes_processed{};
out.reserve(reserve);
metas.reserve(reserve);
if (has_pool)
{
receives.reserve(reserve);
txes_processed.reserve(reserve);
}
db::transaction_link next_output{};
db::transaction_link next_spend{};
if (!output.is_end())
next_output = output.template get_value<MONERO_FIELD(db::output, link)>();
if (!spend.is_end())
next_spend = spend.template get_value<MONERO_FIELD(db::spend, link)>();
while (!output.is_end() || !spend.is_end())
{
if (!out.empty())
{
db::transaction_link const& last = out.back().info.link;
LWS_VERIFY((output.is_end() || last <= next_output) && (spend.is_end() || last <= next_spend));
}
if (spend.is_end() || (!output.is_end() && next_output <= next_spend))
{
LWS_VERIFY(!output.is_end());
std::uint64_t amount = 0;
if (out.empty() || out.back().info.link.tx_hash != next_output.tx_hash)
{
out.push_back({*output});
amount = out.back().info.spend_meta.amount;
if (has_pool)
txes_processed.emplace(next_output.tx_hash);
}
else
{
amount = output.template get_value<MONERO_FIELD(db::output, spend_meta.amount)>();
out.back().info.spend_meta.amount += amount;
}
if (has_pool)
{
auto id = output.template get_value<MONERO_FIELD(db::output, spend_meta.id)>();
auto subaddr = output.template get_value<MONERO_FIELD(db::output, recipient)>();
receives.emplace_back(std::move(id), std::move(subaddr));
}
if (all_outputs)
out.back().receives.push_back(*output);
const db::output::spend_meta_ meta = output.template get_value<MONERO_FIELD(db::output, spend_meta)>();
if (metas.empty() || metas.back().id < meta.id)
metas.push_back(meta);
else
metas.insert(rpc::get_address_txs_response::find_metadata(metas, meta.id), meta);
total += amount;
++output;
if (!output.is_end())
next_output = output.template get_value<MONERO_FIELD(db::output, link)>();
}
else if (output.is_end() || (next_spend < next_output))
{
using meta_type = db::output::spend_meta_;
LWS_VERIFY(!spend.is_end());
const db::output_id source_id = spend.template get_value<MONERO_FIELD(db::spend, source)>();
const auto meta = rpc::get_address_txs_response::find_metadata(metas, source_id);
LWS_VERIFY(skip_spend_meta || (meta != metas.end() && meta->id == source_id));
if (out.empty() || out.back().info.link.tx_hash != next_spend.tx_hash)
{
out.push_back({});
out.back().spends.push_back({skip_spend_meta ? meta_type{} : *meta, *spend});
out.back().info.link.height = out.back().spends.back().possible_spend.link.height;
out.back().info.link.tx_hash = out.back().spends.back().possible_spend.link.tx_hash;
out.back().info.spend_meta.mixin_count =
out.back().spends.back().possible_spend.mixin_count;
out.back().info.timestamp = out.back().spends.back().possible_spend.timestamp;
out.back().info.unlock_time = out.back().spends.back().possible_spend.unlock_time;
if (has_pool)
txes_processed.emplace(out.back().info.link.tx_hash);
}
else
out.back().spends.push_back({skip_spend_meta ? meta_type{} : *meta, *spend});
if (!skip_spend_meta)
out.back().spent += meta->amount;
++spend;
if (!spend.is_end())
next_spend = spend.template get_value<MONERO_FIELD(db::spend, link)>();
}
}
if (has_pool)
{
// Add mempool transactions. Order is not important, since
// mempool txs cannot depend on each other.
lws::account full_user{*user, std::move(receives), {}};
const auto pool_txs = pool->scan_account(full_user);
for (const auto& row: pool_txs)
{
// mempool update after block could be delayed
if (txes_processed.count(row.hash))
continue;
rpc::get_transaction& tx = out.emplace_back();
// Ingore spends that use unknown outputs
// (perhaps the scanner thread is behind, and hasn't seen them yet).
bool from_future = false;
for (const auto& spend: row.spends)
{
const auto meta = rpc::get_address_txs_response::find_metadata(metas, spend.source);
if (meta == metas.end() || meta->id != spend.source)
{
from_future = true;
break;
}
tx.spends.push_back({*meta, spend});
tx.spent += meta->amount;
}
uint64_t tx_total = 0;
for (const auto& output: row.outputs)
{
const auto amount = output.spend_meta.amount;
total += amount;
tx_total += amount;
tx.receives.push_back(output);
}
if (!row.outputs.empty())
{
tx.info = row.outputs.front();
tx.info.spend_meta.amount = tx_total;
}
else
{
if (row.spends.empty() || from_future) break;
auto spend = row.spends.front();
tx.info.link.tx_hash = row.hash;
tx.info.link.height = db::block_id::txpool;
tx.info.spend_meta.amount = tx_total;
tx.info.spend_meta.mixin_count = spend.mixin_count;
tx.info.timestamp = spend.timestamp;
tx.info.unlock_time = spend.unlock_time;
}
}
}
return {std::move(out), total};
}
struct tx_link_sorter_
{
template<typename T>
bool operator()(const T& left, const T& right) const noexcept
{
return left.link < right.link;
}
};
constexpr const tx_link_sorter_ by_tx_link{};
}
std::vector<rpc::get_transaction> rpc::get_address_txs_response::load(std::vector<db::output> outputs, std::vector<db::spend> spends)
{
std::sort(outputs.begin(), outputs.end(), by_tx_link);
std::sort(spends.begin(), spends.end(), by_tx_link);
return merge_into_txes(vec_lmdb(outputs), vec_lmdb(spends), nullptr, nullptr, outputs.size(), true, true).first;
}
expect<rpc::get_address_txs_response> rpc::get_address_txs_response::load(db::storage_reader& reader, const db::account& acct, mempool const* const pool, const bool all_outputs)
{
auto outputs = reader.get_outputs(acct.id);
if (!outputs)
return outputs.error();
auto spends = reader.get_spends(acct.id);
if (!spends)
return spends.error();
const expect<db::block_info> last = reader.get_last_block();
if (!last)
return last.error();
get_address_txs_response resp{};
resp.scanned_height = std::uint64_t(acct.scan_height);
resp.scanned_block_height = resp.scanned_height;
resp.start_height = std::uint64_t(acct.start_height);
resp.blockchain_height = std::uint64_t(last->id);
resp.transaction_height = resp.blockchain_height;
resp.lookahead_fail = db::to_uint(acct.lookahead_fail);
resp.lookahead = acct.lookahead;
auto out = merge_into_txes(outputs->make_iterator(), spends->make_iterator(), std::addressof(acct), pool, outputs->count(), all_outputs, false);
resp.total_received = safe_uint64(out.second);
resp.transactions = std::move(out.first);
return resp;
}
void rpc::write_bytes(wire::json_writer& dest, const get_address_txs_response& self)
{
wire::object(dest,
@@ -457,7 +949,7 @@ namespace lws
);
convert_address(address, self.creds.address);
}
void rpc::read_bytes(wire::json_reader& source, submit_raw_tx_request& self)
{
wire::object(source, WIRE_FIELD(tx));
+126 -39
View File
@@ -29,6 +29,7 @@
#include <boost/optional/optional.hpp>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
@@ -38,9 +39,12 @@
#include "cryptonote_basic/difficulty.h" // monero/src
#include "crypto/crypto.h" // monero/src
#include "db/data.h"
#include "db/fwd.h"
#include "rpc/fwd.h"
#include "rpc/rates.h"
#include "util/fwd.h"
#include "wire/json/fwd.h"
#include "wire/msgpack/fwd.h"
namespace lws
{
@@ -64,7 +68,7 @@ namespace rpc
lws::db::account_address address;
crypto::secret_key key;
};
void read_bytes(wire::json_reader&, account_credentials&);
void read_bytes(wire::reader&, account_credentials&);
enum class daemon_state : std::uint8_t
@@ -107,15 +111,6 @@ namespace rpc
void write_bytes(wire::json_writer&, const daemon_status_response&);
struct new_subaddrs_response
{
new_subaddrs_response() = delete;
std::vector<db::subaddress_dict> new_subaddrs;
std::vector<db::subaddress_dict> all_subaddrs;
};
void write_bytes(wire::json_writer&, const new_subaddrs_response&);
struct transaction_spend
{
transaction_spend() = delete;
@@ -124,6 +119,121 @@ namespace rpc
};
void write_bytes(wire::json_writer&, const transaction_spend&);
struct get_transaction
{
get_transaction()
: info{}, receives(), spends(), spent(0)
{}
get_transaction(const db::output& in)
: info(in), receives(), spends(), spent(0)
{}
db::output info;
std::vector<db::output> receives;
std::vector<transaction_spend> spends;
std::uint64_t spent;
};
struct get_address_txs_response
{
get_address_txs_response() = delete;
static std::vector<db::output::spend_meta_>::const_iterator
find_metadata(std::vector<db::output::spend_meta_> const& metas, db::output_id id);
static std::vector<get_transaction> load(std::vector<db::output> outputs, std::vector<db::spend> spends);
static expect<get_address_txs_response> load(db::storage_reader& reader, const db::account& acct, const mempool* pool, const bool all_outputs);
safe_uint64 total_received;
std::uint64_t scanned_height;
std::uint64_t scanned_block_height;
std::uint64_t start_height;
std::uint64_t transaction_height;
std::uint64_t blockchain_height;
std::uint64_t lookahead_fail;
std::vector<get_transaction> transactions;
db::address_index lookahead;
};
void write_bytes(wire::json_writer&, const get_address_txs_response&);
struct feed_blocks
{
static constexpr const char* prefix() noexcept { return "blocks:"; }
feed_blocks() = delete;
db::block_id scan_start;
db::block_id scan_end;
db::block_id blockchain_height;
db::block_id lookahead_fail;
db::address_index lookahead;
std::vector<get_transaction> transactions;
};
void write_bytes(wire::writer&, const feed_blocks&);
struct feed_error
{
static constexpr const char* prefix() noexcept { return "error:"; }
feed_error() = delete;
feed_error(std::error_code);
std::string msg;
feed::status code;
};
void write_bytes(wire::writer&, const feed_error&);
struct feed_login
{
static constexpr const char* prefix() noexcept { return "login:"; }
feed_login() = delete;
account_credentials account;
bool tx_sync;
bool receives_only;
};
void read_bytes(wire::reader&, feed_login&);
struct feed_mempool
{
static constexpr const char* prefix() noexcept { return "mempool:"; }
feed_mempool() = delete;
db::output received;
};
void write_bytes(wire::writer&, const feed_mempool&);
struct feed_tx_sync
{
static constexpr const char* prefix() noexcept { return "tx_sync:"; }
feed_tx_sync() = delete;
get_address_txs_response info;
};
void write_bytes(wire::writer&, const feed_tx_sync&);
struct feed_warning
{
static constexpr const char* prefix() noexcept { return "warning:"; }
feed_warning()
: msg(), code(feed::status(0)), counter(0), height(db::block_id::txpool)
{}
feed_warning(std::error_code error, const std::uint32_t counter, const db::block_id height);
std::string msg;
feed::status code;
std::uint32_t counter;
db::block_id height;
};
void read_bytes(wire::msgpack_reader&, feed_warning&);
void write_bytes(wire::writer&, const feed_warning&);
struct new_subaddrs_response
{
new_subaddrs_response() = delete;
std::vector<db::subaddress_dict> new_subaddrs;
std::vector<db::subaddress_dict> all_subaddrs;
};
void write_bytes(wire::json_writer&, const new_subaddrs_response&);
struct get_address_info_response
{
@@ -138,7 +248,8 @@ namespace rpc
blockchain_height(0),
lookahead_fail(0),
spent_outputs(),
rates(common_error::kInvalidArgument)
rates(common_error::kInvalidArgument),
lookahead{}
{}
safe_uint64 locked_funds;
@@ -155,31 +266,7 @@ namespace rpc
db::address_index lookahead;
};
void write_bytes(wire::json_writer&, const get_address_info_response&);
struct get_address_txs_response
{
get_address_txs_response() = delete;
struct transaction
{
transaction() = delete;
db::output info;
std::vector<transaction_spend> spends;
std::uint64_t spent;
};
safe_uint64 total_received;
std::uint64_t scanned_height;
std::uint64_t scanned_block_height;
std::uint64_t start_height;
std::uint64_t transaction_height;
std::uint64_t blockchain_height;
std::uint64_t lookahead_fail;
std::vector<transaction> transactions;
db::address_index lookahead;
};
void write_bytes(wire::json_writer&, const get_address_txs_response&);
struct get_random_outs_request
{
@@ -298,7 +385,7 @@ namespace rpc
db::address_index lookahead;
};
void write_bytes(wire::json_writer&, login_response);
struct provision_subaddrs_request
{
@@ -310,9 +397,9 @@ namespace rpc
boost::optional<std::uint32_t> n_min;
boost::optional<bool> get_all;
};
void read_bytes(wire::json_reader&, provision_subaddrs_request&);
void read_bytes(wire::json_reader&, provision_subaddrs_request&);
struct submit_raw_tx_request
{
submit_raw_tx_request() = delete;
+80
View File
@@ -0,0 +1,80 @@
// Copyright (c) 2026, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "login.h"
#include "crypto/crypto.h" // monero/src
#include "db/data.h"
#include "db/storage.h"
#include "error.h"
#include "rpc/light_wallet.h"
namespace lws { namespace rpc
{
bool is_hidden(db::account_status status) noexcept
{
switch (status)
{
case db::account_status::active:
case db::account_status::inactive:
return false;
default:
case db::account_status::hidden:
break;
}
return true;
}
bool key_check(const rpc::account_credentials& creds)
{
crypto::public_key verify{};
if (!crypto::secret_key_to_public_key(creds.key, verify))
return false;
if (verify != creds.address.view_public)
return false;
return true;
}
//! \return Account info from the DB, iff key matches address AND address is NOT hidden.
expect<std::pair<db::account, db::storage_reader>> open_account(const account_credentials& creds, const db::storage& disk)
{
if (!key_check(creds))
return {lws::error::bad_view_key};
auto reader = disk.start_read();
if (!reader)
return reader.error();
const auto user = reader->get_account(creds.address);
if (!user)
return user.error();
if (is_hidden(user->first))
return {lws::error::account_not_found};
return {std::make_pair(user->second, std::move(*reader))};
}
}} // lws // rpc
+45
View File
@@ -0,0 +1,45 @@
// Copyright (c) 2026, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <utility>
#include "common/expect.h" // monero/src
#include "db/fwd.h"
#include "rpc/fwd.h"
namespace lws { namespace rpc
{
bool is_hidden(db::account_status status) noexcept;
//! Verify that view pub matches view secret.
bool key_check(const rpc::account_credentials& creds);
//! \return Account info from the DB, iff key matches address AND address is NOT hidden.
expect<std::pair<db::account, db::storage_reader>> open_account(const account_credentials& creds, const db::storage& disk);
}} // lws // rpc
+2 -2
View File
@@ -623,9 +623,9 @@ namespace lws { namespace rpc { namespace scanner
std::sort(users.begin(), users.end(), by_height{});
boost::asio::dispatch(
self->strand_,
[self, users = std::move(users), blocks = std::move(blocks)] ()
[self, users = std::move(users), blocks = std::move(blocks)] () mutable
{
if (!lws::user_data::store(self->strand_.context(), self->disk_, self->zclient_, self->webhook_, epee::to_span(blocks), epee::to_span(users), nullptr))
if (!lws::user_data::store(self->strand_.context(), self->disk_, self->zclient_, self->webhook_, epee::to_span(blocks), epee::to_mut_span(users), nullptr))
{
self->do_stop();
self->strand_.context().stop();
+99 -390
View File
@@ -54,6 +54,7 @@
#include "db/data.h"
#include "cryptonote_basic/difficulty.h" // monero/src
#include "error.h"
#include "mempool.h"
#include "hardforks/hardforks.h" // monero/src
#include "misc_log_ex.h" // monero/contrib/epee/include
#include "net/net_parse_helpers.h"
@@ -68,6 +69,7 @@
#include "rpc/scanner/server.h"
#include "rpc/webhook.h"
#include "util/blocks.h"
#include "util/ownership_test.h"
#include "util/source_location.h"
#include "util/transactions.h"
@@ -195,18 +197,21 @@ namespace lws
struct add_spend
{
void operator()(lws::account& user, const db::spend& spend) const
void operator()(lws::account& user, db::spend&& spend) const
{ user.add_spend(spend); }
};
struct add_output
{
bool operator()(expect<db::storage_reader>&, lws::account& user, const db::output& out) const
{ return user.add_out(out); }
void operator()(lws::account& user, db::output&& out) const
{
if (!user.add_out(out))
MWARNING("Output not added, duplicate public key encountered");
}
};
struct null_spend
{
void operator()(lws::account&, const db::spend&) const noexcept
void operator()(lws::account&, db::spend&&) const noexcept
{}
};
struct send_webhook
@@ -214,387 +219,41 @@ namespace lws
db::storage const& disk_;
rpc::client& client_;
scanner_sync& http_;
std::unordered_map<crypto::hash, crypto::hash> txpool_;
bool operator()(expect<db::storage_reader>& reader, lws::account& user, const db::output& out)
void operator()(lws::account& user, db::output&& out)
{
/* Upstream monerod does not send all fields for a transaction, so
mempool notifications cannot compute tx_hash correctly (it is not
sent separately, a further blunder). Instead, if there are matching
outputs with webhooks, fetch mempool to compare tx_prefix_hash and
then use corresponding tx_hash. */
const db::webhook_key key{user.id(), db::webhook_type::tx_confirmation};
std::vector<db::webhook_value> hooks{};
{
db::storage_reader* active_reader = reader ?
std::addressof(*reader) : nullptr;
expect<db::storage_reader> temp_reader{common_error::kInvalidArgument};
if (!active_reader)
expect<db::storage_reader> reader = disk_.start_read();
if (!reader)
{
temp_reader = disk_.start_read();
if (!temp_reader)
{
MERROR("Unable to lookup webhook on tx in pool: " << reader.error().message());
return false;
}
active_reader = std::addressof(*temp_reader);
MERROR("Unable to lookup webhook on tx in pool: " << reader.error().message());
return;
}
auto found = active_reader->find_webhook(key, out.payment_id.short_);
auto found = reader->find_webhook(key, out.payment_id.short_);
if (!found)
{
MERROR("Failed db lookup for webhooks: " << found.error().message());
return false;
return;
}
hooks = std::move(*found);
}
if (!hooks.empty() && txpool_.empty())
{
cryptonote::rpc::GetTransactionPool::Request req{};
if (!send(client_, rpc::client::make_message("get_transaction_pool", req)))
{
MERROR("Unable to compute tx hash for webhook, aborting");
return false;
}
auto resp = client_.get_message(std::chrono::seconds{3});
if (!resp)
{
MERROR("Unable to get txpool: " << resp.error().message());
return false;
}
auto txpool = rpc::parse_json_response<rpc::get_transaction_pool>(std::move(*resp));
if (!txpool)
MONERO_THROW(txpool.error(), "Failed fetching transaction pool");
for (auto& tx : txpool->transactions)
txpool_.emplace(get_transaction_prefix_hash(tx.tx), tx.tx_hash);
}
std::vector<db::webhook_tx_confirmation> events{};
for (auto& hook : hooks)
{
events.push_back(db::webhook_tx_confirmation{key, std::move(hook), out});
events.back().value.second.confirmations = 0;
const auto hash = txpool_.find(out.tx_prefix_hash);
if (hash != txpool_.end())
events.back().tx_info.link.tx_hash = hash->second;
else
events.pop_back(); //cannot compute tx_hash
}
send_payment_hook(http_.io_, client_, http_.webhooks_, epee::to_span(events));
return true;
const expect<void> pushed = client_.push_update(user, mempool_receive{out});
if (!pushed)
MERROR("Failed to send mempool update to " << user.address() << ": " << pushed.error().message());
}
};
struct subaddress_reader
{
expect<db::storage_reader> reader;
std::optional<db::storage> disk;
db::cursor::subaddress_indexes cur;
const std::uint32_t max_subaddresses;
subaddress_reader(std::optional<db::storage> const& disk_in, const std::uint32_t max_subaddresses)
: reader(common_error::kInvalidArgument), disk(), cur(nullptr), max_subaddresses(max_subaddresses)
{
if (disk_in)
disk = disk_in->clone();
if (max_subaddresses)
update_reader();
}
void update_reader()
{
if (disk)
reader = disk->start_read();
if (!reader)
MERROR("Subadress lookup failure: " << reader.error().message());
}
};
void update_lookahead(const account& user, subaddress_reader& reader, const db::address_index& match, const db::block_id height)
{
if (match.is_zero())
return; // keep subaddress disabled servers quick
if (!reader.disk)
throw std::runtime_error{"Bad DB handle in scanner"};
auto upserted = reader.disk->update_lookahead(user.db_address(), height, match, reader.max_subaddresses);
if (upserted)
{
if (0 < *upserted)
reader.update_reader(); // update reader after upsert added new addresses
else if (*upserted < 0)
upserted = {error::max_subaddresses};
}
if (!upserted)
MWARNING("Failed to update lookahead for " << user.address() << ": " << upserted.error());
}
void scan_transaction_base(
epee::span<lws::account> users,
const db::block_id height,
const std::uint64_t timestamp,
crypto::hash const& tx_hash,
cryptonote::transaction const& tx,
std::vector<std::uint64_t> const& out_ids,
subaddress_reader& reader,
std::function<void(lws::account&, const db::spend&)> spend_action,
std::function<bool(expect<db::storage_reader>&, lws::account&, const db::output&)> output_action)
{
if (2 < tx.version)
throw std::runtime_error{"Unsupported tx version"};
cryptonote::tx_extra_pub_key key;
boost::optional<crypto::hash> prefix_hash;
boost::optional<cryptonote::tx_extra_nonce> extra_nonce;
std::pair<std::uint8_t, db::output::payment_id_> payment_id;
cryptonote::tx_extra_additional_pub_keys additional_tx_pub_keys;
std::vector<crypto::key_derivation> additional_derivations;
{
std::vector<cryptonote::tx_extra_field> extra;
cryptonote::parse_tx_extra(tx.extra, extra);
// allow partial parsing of tx extra (similar to wallet2.cpp)
if (!cryptonote::find_tx_extra_field_by_type(extra, key))
return;
extra_nonce.emplace();
if (cryptonote::find_tx_extra_field_by_type(extra, *extra_nonce))
{
if (cryptonote::get_payment_id_from_tx_extra_nonce(extra_nonce->nonce, payment_id.second.long_))
payment_id.first = sizeof(crypto::hash);
}
else
extra_nonce = boost::none;
// additional tx pub keys present when there are 3+ outputs in a tx involving subaddresses
if (reader.reader)
cryptonote::find_tx_extra_field_by_type(extra, additional_tx_pub_keys);
} // destruct `extra` vector
for (account& user : users)
{
if (height <= user.scan_height())
continue; // to next user
crypto::key_derivation derived;
if (!crypto::wallet::generate_key_derivation(key.pub_key, user.view_key(), derived))
continue; // to next user
if (reader.reader && additional_tx_pub_keys.data.size() == tx.vout.size())
{
additional_derivations.resize(tx.vout.size());
std::size_t index = -1;
for (auto const& out: tx.vout)
{
++index;
if (!crypto::wallet::generate_key_derivation(additional_tx_pub_keys.data[index], user.view_key(), additional_derivations[index]))
{
additional_derivations.clear();
break; // vout loop
}
}
}
db::extra ext{};
std::uint32_t mixin = 0;
for (auto const& in : tx.vin)
{
cryptonote::txin_to_key const* const in_data =
boost::get<cryptonote::txin_to_key>(std::addressof(in));
if (in_data)
{
mixin = boost::numeric_cast<std::uint32_t>(
std::max(std::size_t(1), in_data->key_offsets.size()) - 1
);
std::uint64_t goffset = 0;
for (std::uint64_t offset : in_data->key_offsets)
{
goffset += offset;
const boost::optional<db::address_index> subaccount =
user.get_spendable(db::output_id{in_data->amount, goffset});
if (!subaccount)
continue; // to next input
spend_action(
user,
db::spend{
db::transaction_link{height, tx_hash},
in_data->k_image,
db::output_id{in_data->amount, goffset},
timestamp,
tx.unlock_time,
mixin,
{0, 0, 0}, // reserved
payment_id.first,
payment_id.second.long_,
*subaccount
}
);
}
}
else if (boost::get<cryptonote::txin_gen>(std::addressof(in)))
ext = db::extra(ext | db::coinbase_output);
}
std::size_t index = -1;
for (auto const& out : tx.vout)
{
++index;
crypto::public_key out_pub_key;
if (!cryptonote::get_output_public_key(out, out_pub_key))
continue; // to next output
boost::optional<crypto::view_tag> view_tag_opt =
cryptonote::get_output_view_tag(out);
const bool found_tag =
(!additional_derivations.empty() && cryptonote::out_can_be_to_acc(view_tag_opt, additional_derivations.at(index), index)) ||
cryptonote::out_can_be_to_acc(view_tag_opt, derived, index);
if (!found_tag)
continue; // to next output
bool found_pub = false;
db::address_index account_index{db::major_index::primary, db::minor_index::primary};
crypto::key_derivation active_derived{};
crypto::public_key active_pub{};
// inspect the additional and traditional keys
for (std::size_t attempt = 0; attempt < 2; ++attempt)
{
if (attempt == 0)
{
active_derived = derived;
active_pub = key.pub_key;
}
else if (!additional_derivations.empty())
{
active_derived = additional_derivations.at(index);
active_pub = additional_tx_pub_keys.data.at(index);
}
else
break; // inspection loop
crypto::public_key derived_pub;
if (!crypto::wallet::derive_subaddress_public_key(out_pub_key, active_derived, index, derived_pub))
continue; // to next available active_derived
if (user.spend_public() != derived_pub)
{
if (!reader.reader)
continue; // to next available active_derived
const expect<db::address_index> match =
reader.reader->find_subaddress(user.id(), derived_pub, reader.cur);
if (!match)
{
if (match != lmdb::error(MDB_NOTFOUND))
MERROR("Failure when doing subaddress search: " << match.error().message());
continue; // to next available active_derived
}
update_lookahead(user, reader, *match, height);
found_pub = true;
account_index = *match;
break; // additional_derivations loop
}
else
{
found_pub = true;
break; // additional_derivations loop
}
}
if (!found_pub)
continue; // to next output
if (!prefix_hash)
{
prefix_hash.emplace();
cryptonote::get_transaction_prefix_hash(tx, *prefix_hash);
}
std::uint64_t amount = out.amount;
rct::key mask = rct::identity();
if (!amount && !(ext & db::coinbase_output) && 1 < tx.version)
{
const bool bulletproof2 = (rct::RCTTypeBulletproof2 <= tx.rct_signatures.type);
const auto decrypted = lws::decode_amount(
tx.rct_signatures.outPk.at(index).mask, tx.rct_signatures.ecdhInfo.at(index), active_derived, index, bulletproof2, tx.rct_signatures.type
);
if (!decrypted)
{
MWARNING(user.address() << " failed to decrypt amount for tx " << tx_hash << ", skipping output");
continue; // to next output
}
amount = decrypted->first;
mask = decrypted->second;
ext = db::extra(ext | db::ringct_output);
}
else if (1 < tx.version)
ext = db::extra(ext | db::ringct_output);
if (extra_nonce)
{
if (!payment_id.first && cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce->nonce, payment_id.second.short_))
{
payment_id.first = sizeof(crypto::hash8);
lws::decrypt_payment_id(payment_id.second.short_, active_derived);
}
}
const bool added = output_action(
reader.reader,
user,
db::output{
db::transaction_link{height, tx_hash},
db::output::spend_meta_{
db::output_id{tx.version < 2 ? out.amount : 0, out_ids.at(index)},
amount,
mixin,
boost::numeric_cast<std::uint32_t>(index),
active_pub
},
timestamp,
tx.unlock_time,
*prefix_hash,
out_pub_key,
mask,
{0, 0, 0, 0, 0, 0, 0}, // reserved bytes
db::pack(ext, payment_id.first),
payment_id.second,
cryptonote::get_tx_fee(tx),
account_index
}
);
if (!added)
MWARNING("Output not added, duplicate public key encountered");
} // for all tx outs
} // for all users
}
void scan_transaction(
epee::span<lws::account> users,
const db::block_id height,
const std::uint64_t timestamp,
crypto::hash const& tx_hash,
cryptonote::transaction const& tx,
std::vector<std::uint64_t> const& out_ids,
subaddress_reader& reader)
{
scan_transaction_base(users, height, timestamp, tx_hash, tx, out_ids, reader, add_spend{}, add_output{});
}
void scan_transactions(std::string&& txpool_msg, epee::span<lws::account> users, db::storage const& disk, scanner_sync& self, rpc::client& client, const scanner_options& opts)
{
// uint64::max is for txpool
@@ -612,10 +271,11 @@ namespace lws
const auto time =
boost::numeric_cast<std::uint64_t>(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()));
subaddress_reader reader{std::optional<db::storage>{disk.clone()}, opts.max_subaddresses};
send_webhook sender{disk, client, self};
ownership_test scan_transaction{null_spend{}, send_webhook{disk, client, self}};
if (opts.max_subaddresses > 0)
scan_transaction.enable_subaddresses(disk, opts.max_subaddresses);
for (const auto& tx : parsed->txes)
scan_transaction_base(users, db::block_id::txpool, time, crypto::hash{}, tx, fake_outs, reader, null_spend{}, sender);
scan_transaction(users, db::block_id::txpool, time, nullptr, tx, fake_outs);
}
void do_scan_loop(scanner_sync& self, std::shared_ptr<thread_data> data, const size_t thread_n) noexcept
@@ -879,7 +539,9 @@ namespace lws
);
}
subaddress_reader reader{disk, opts.max_subaddresses};
ownership_test scan_transaction{add_spend{}, add_output{}};
if (disk && opts.max_subaddresses)
scan_transaction.enable_subaddresses(*disk, opts.max_subaddresses);
db::block_difficulty::unsigned_int diff{};
const db::block_id initial_height = db::block_id(fetched->start_height);
for (auto block_data : boost::combine(blocks, indices))
@@ -896,18 +558,13 @@ namespace lws
if (indices.empty())
throw std::runtime_error{"Bad daemon response - missing /coinbase tx indices"};
crypto::hash miner_tx_hash;
if (!cryptonote::get_transaction_hash(block.miner_tx, miner_tx_hash))
throw std::runtime_error{"Failed to calculate miner tx hash"};
scan_transaction(
epee::to_mut_span(users),
db::block_id(fetched->start_height),
block.timestamp,
miner_tx_hash,
nullptr,
block.miner_tx,
*(indices.begin()),
reader
*(indices.begin())
);
if (opts.untrusted_daemon)
@@ -956,10 +613,9 @@ namespace lws
epee::to_mut_span(users),
db::block_id(fetched->start_height),
block.timestamp,
boost::get<0>(tx_data),
boost::get<1>(tx_data),
boost::get<2>(tx_data),
reader
std::addressof(boost::get<0>(tx_data)), // tx_hashes
boost::get<1>(tx_data), // txes
boost::get<2>(tx_data) // indices
);
}
@@ -978,13 +634,12 @@ namespace lws
blockchain.push_back(cryptonote::get_block_hash(block));
} // for each block
reader.reader = std::error_code{common_error::kInvalidArgument}; // cleanup reader before next write
MINFO("Thread " << thread_n << " processed " << blockchain.size() << " blocks(s) @ height " << fetched->start_height << " against " << users.size() << " account(s)");
if (!blockchain.empty())
MINFO("First block hash: " << blockchain.front() << " at height " << (fetched->start_height - blockchain.size() + 1) << ", last block hash: " << blockchain.back() << " at height " << fetched->start_height);
if (!store(self.io_, client, self.webhooks_, epee::to_span(blockchain), epee::to_span(users), epee::to_span(new_pow)))
scan_transaction.disable_subaddresses(); // cleanup reader before next write
if (!store(self.io_, client, self.webhooks_, epee::to_span(blockchain), epee::to_mut_span(users), epee::to_span(new_pow)))
return false;
// TODO
@@ -993,9 +648,6 @@ namespace lws
MINFO("On chain with hash " << blockchain.back() << " and difficulty " << diff << " at height " << fetched->start_height);
}
for (account& user : users)
user.updated(db::block_id(fetched->start_height));
// Update queue with current minimum scan height (oldest account determines thread progress)
queue.update_min_height(users.front().scan_height());
}
@@ -1010,7 +662,17 @@ namespace lws
Launches `thread_count` threads to run `scan_loop`, and then polls for
active account changes in background
*/
void check_loop(scanner_sync& self, db::storage disk, rpc::context& ctx, const std::size_t thread_count, const std::string& lws_server_addr, std::string lws_server_pass, std::vector<lws::account> users, std::vector<db::account_id> active, const scanner_options& opts)
void check_loop(
scanner_sync& self,
db::storage disk,
rpc::context& ctx,
std::shared_ptr<lws::mempool> pool,
const std::size_t thread_count,
const std::string& lws_server_addr,
std::string lws_server_pass,
std::vector<lws::account> users,
std::vector<db::account_id> active,
const scanner_options& opts)
{
assert(users.size() == active.size());
assert(thread_count || !lws_server_addr.empty());
@@ -1293,6 +955,35 @@ namespace lws
}
}
if (pool) {
auto client = std::make_shared<rpc::client>(MONERO_UNWRAP(ctx.connect()));
MONERO_UNWRAP(client->watch_scan_signals());
threads.emplace_back(attrs, [pool, client, &self] ()
{
MINFO("Pool updater thread started");
while (self.is_running())
{
try
{
auto result = pool_update_loop(pool, *client);
if (!result
&& result.error() != make_error_code(lws::error::signal_abort_process)
&& result.error() != make_error_code(lws::error::signal_abort_scan))
MERROR("Pool update loop failed " << result.error());
}
catch (std::exception const& e)
{
MERROR("Pool update threw " << e.what());
}
catch (...)
{
MERROR("Pool update threw unknown exception");
}
}
MINFO("Pool updater thread stopped");
});
}
users.clear();
users.shrink_to_fit();
@@ -1377,6 +1068,7 @@ namespace lws
req.start_height = 0;
req.known_hashes = MONERO_UNWRAP(MONERO_UNWRAP(disk.start_read()).get_chain_sync());
bool sent_warning = false;
for (;;)
{
if (req.known_hashes.empty())
@@ -1397,6 +1089,9 @@ namespace lws
MINFO("Syncing " << resp->hashes.size() << " hashes from height " << resp->start_height);
MONERO_CHECK(disk.sync_chain(db::block_id(resp->start_height), epee::to_span(resp->hashes), regtest));
if (!sent_warning)
MONERO_UNWRAP(client.push_warning(error::blockchain_reorg, db::block_id(resp->start_height)));
sent_warning = true;
req.known_hashes.erase(req.known_hashes.begin(), --(req.known_hashes.end()));
for (std::size_t num = 0; num < 10; ++num)
@@ -1421,6 +1116,7 @@ namespace lws
req.block_ids = MONERO_UNWRAP(MONERO_UNWRAP(disk.start_read()).get_pow_sync());
req.prune = true;
bool sent_warning = false;
std::vector<crypto::hash> new_hashes{};
std::vector<db::pow_sync> new_pow{};
for (;;)
@@ -1458,7 +1154,7 @@ namespace lws
return {error::bad_daemon_response};
return {std::move(client)};
}
// genesis block must be present as last entry
req.block_ids.erase(req.block_ids.begin(), --(req.block_ids.end()));
@@ -1541,20 +1237,25 @@ namespace lws
MONERO_CHECK(disk.sync_pow(db::block_id(resp->start_height), epee::to_span(new_hashes), epee::to_span(new_pow)));
MINFO("Verified up to block " << (resp->start_height + new_hashes.size() - 1) << " with hash " << hash << " and difficulty " << diff);
if (!sent_warning)
MONERO_UNWRAP(client.push_warning(error::blockchain_reorg, db::block_id(resp->start_height)));
sent_warning = true;
} // for until sync
return {std::move(client)};
}
} // anonymous
bool user_data::store(boost::asio::io_context& io, db::storage& disk, rpc::client& client, net::http::client& webhook, const epee::span<const crypto::hash> chain, const epee::span<const lws::account> users, const epee::span<const db::pow_sync> pow)
bool user_data::store(boost::asio::io_context& io, db::storage& disk, rpc::client& client, net::http::client& webhook, const epee::span<const crypto::hash> chain, const epee::span<lws::account> users, const epee::span<const db::pow_sync> pow)
{
if (users.empty())
return true;
if (!std::is_sorted(users.begin(), users.end(), by_height{}))
throw std::logic_error{"users must be sorted!"};
LWS_VERIFY(!chain.empty());
LWS_VERIFY(std::is_sorted(users.begin(), users.end(), by_height{}));
auto updated = disk.update(users[0].scan_height(), chain, users, pow);
const db::block_id base_height = users[0].scan_height();
auto updated = disk.update(base_height, chain, epee::to_span(users), pow);
if (!updated)
{
if (updated == lws::error::blockchain_reorg)
@@ -1567,6 +1268,7 @@ namespace lws
send_payment_hook(io, client, webhook, epee::to_span(updated->confirm_pubs));
send_spend_hook(io, client, webhook, epee::to_span(updated->spend_pubs));
client.push_updates(users, db::block_id(to_uint(base_height) + chain.size() - 1));
if (updated->accounts_updated != users.size())
{
MWARNING("Only updated " << updated->accounts_updated << " account(s) out of " << users.size() << ", resetting");
@@ -1581,7 +1283,7 @@ namespace lws
return true;
}
bool user_data::operator()(boost::asio::io_context& io, rpc::client& client, net::http::client& webhook, const epee::span<const crypto::hash> chain, const epee::span<const lws::account> users, const epee::span<const db::pow_sync> pow)
bool user_data::operator()(boost::asio::io_context& io, rpc::client& client, net::http::client& webhook, const epee::span<const crypto::hash> chain, const epee::span<lws::account> users, const epee::span<const db::pow_sync> pow)
{
return store(io, disk_, client, webhook, chain, users, pow);
}
@@ -1595,7 +1297,13 @@ namespace lws
return sync_quick(sync_, disk_.clone(), std::move(client), regtest);
}
void scanner::run(rpc::context ctx, std::size_t thread_count, const std::string& lws_server_addr, std::string lws_server_pass, const scanner_options& opts)
void scanner::run(
rpc::context ctx,
std::shared_ptr<mempool> pool,
std::size_t thread_count,
const std::string& lws_server_addr,
std::string lws_server_pass,
const scanner_options& opts)
{
if (has_shutdown())
MONERO_THROW(common_error::kInvalidArgument, "this has shutdown");
@@ -1683,7 +1391,7 @@ namespace lws
}
}
else
check_loop(sync_, disk_.clone(), ctx, thread_count, lws_server_addr, lws_server_pass, std::move(users), std::move(active), opts);
check_loop(sync_, disk_.clone(), ctx, pool, thread_count, lws_server_addr, lws_server_pass, std::move(users), std::move(active), opts);
if (has_shutdown())
return;
@@ -1698,6 +1406,7 @@ namespace lws
MONERO_THROW(synced.error(), "Unable to sync blockchain");
MWARNING("Failed to connect to daemon at " << ctx.daemon_address());
MONERO_UNWRAP(ctx.push_warning(synced.error()));
}
else
client = std::move(*synced);
+11 -4
View File
@@ -35,6 +35,7 @@
#include "db/fwd.h"
#include "db/storage.h"
#include "fwd.h"
#include "net/http/client.h"
#include "net/net_ssl.h" // monero/contrib/epee/include
#include "rpc/client.h"
@@ -77,10 +78,10 @@ namespace lws
/*! Store updated accounts locally (`disk`), and send ZMQ/RMQ/webhook
events. `users` must be sorted by height (lowest first). */
static bool store(boost::asio::io_context& io, db::storage& disk, rpc::client& zclient, net::http::client& webhook ,epee::span<const crypto::hash> chain, epee::span<const lws::account> users, epee::span<const db::pow_sync> pow);
static bool store(boost::asio::io_context& io, db::storage& disk, rpc::client& zclient, net::http::client& webhook ,epee::span<const crypto::hash> chain, epee::span<lws::account> users, epee::span<const db::pow_sync> pow);
//! `users` must be sorted by height (lowest first)
bool operator()(boost::asio::io_context& io, rpc::client& zclient, net::http::client& webhook, epee::span<const crypto::hash> chain, epee::span<const lws::account> users, epee::span<const db::pow_sync> pow);
bool operator()(boost::asio::io_context& io, rpc::client& zclient, net::http::client& webhook, epee::span<const crypto::hash> chain, epee::span<lws::account> users, epee::span<const db::pow_sync> pow);
};
struct scanner_sync
@@ -117,7 +118,7 @@ namespace lws
~scanner();
//! Callback for storing user account (typically local lmdb, but perhaps remote rpc)
using store_func = std::function<bool(boost::asio::io_context&, rpc::client&, net::http::client&, epee::span<const crypto::hash>, epee::span<const lws::account>, epee::span<const db::pow_sync>)>;
using store_func = std::function<bool(boost::asio::io_context&, rpc::client&, net::http::client&, epee::span<const crypto::hash>, epee::span<lws::account>, epee::span<const db::pow_sync>)>;
/*! Run _just_ the inner scanner loop while `self.is_running() == true`.
*
@@ -130,7 +131,13 @@ namespace lws
expect<rpc::client> sync(rpc::client client, const bool untrusted_daemon = false, const bool regtest = false);
//! Poll daemon until `shutdown()` is called, using `thread_count` threads.
void run(rpc::context ctx, std::size_t thread_count, const std::string& server_addr, std::string server_pass, const scanner_options&);
void run(
rpc::context ctx,
std::shared_ptr<mempool> pool,
std::size_t thread_count,
const std::string& server_addr,
std::string server_pass,
const scanner_options&);
//! \return True iff `stop()` and `shutdown()` has never been called
bool is_running() const noexcept { return sync_.is_running(); }
+25 -2
View File
@@ -45,6 +45,7 @@
#include "db/storage.h"
#include "error.h"
#include "lws_version.h"
#include "mempool.h"
#include "options.h"
#include "rest_server.h"
#include "scanner.h"
@@ -92,6 +93,7 @@ namespace
const command_line::arg_descriptor<double> split_sync_threads;
const command_line::arg_descriptor<std::uint64_t> split_sync_depth;
const command_line::arg_descriptor<bool> balance_new_addresses;
const command_line::arg_descriptor<std::chrono::seconds::rep> feed_timeout;
static std::string get_default_zmq()
{
@@ -147,6 +149,7 @@ namespace
, split_sync_threads{"split-sync-threads", "Percentage of threads to use for fully synced accounts (0-1, requires --block-depth-threading, 0 to disable)", 0.0}
, split_sync_depth{"split-sync-depth", "Maximum block depth for an address to be considered synced (defaults to 10)", 10}
, balance_new_addresses{"balance-new-addresses", "Assign new addresses to thread with highest blockheight (or fewest addresses if tied)", false}
, feed_timeout{"feed-timeout", "Seconds for timing out inactive websocket '/feed' clients. val <= 0 disables '/feed'", std::chrono::seconds{lws::config::feed_timeout}.count()}
{}
void prepare(boost::program_options::options_description& description) const
@@ -190,6 +193,7 @@ namespace
command_line::add_arg(description, split_sync_threads);
command_line::add_arg(description, split_sync_depth);
command_line::add_arg(description, balance_new_addresses);
command_line::add_arg(description, feed_timeout);
}
};
@@ -292,6 +296,7 @@ namespace
lws::rest_server::configuration{
{command_line::get_arg(args, opts.rest_ssl_key), command_line::get_arg(args, opts.rest_ssl_cert)},
command_line::get_arg(args, opts.access_controls),
std::chrono::seconds{command_line::get_arg(args, opts.feed_timeout)},
command_line::get_arg(args, opts.rest_threads),
command_line::get_arg(args, opts.max_subaddresses),
webhook_verify,
@@ -356,19 +361,36 @@ namespace
boost::filesystem::create_directories(prog.db_path);
auto disk = lws::db::storage::open(prog.db_path.c_str(), prog.create_queue_max);
auto ctx = lws::rpc::context::make(std::move(prog.daemon_rpc), std::move(prog.daemon_sub), std::move(prog.zmq_pub), std::move(prog.rmq), prog.rates_interval, prog.untrusted_daemon);
auto ctx = lws::rpc::context::make(
std::move(prog.daemon_rpc),
std::move(prog.daemon_sub),
std::move(prog.zmq_pub),
std::move(prog.rmq),
prog.rates_interval,
prog.untrusted_daemon,
std::chrono::seconds{0} < prog.rest_config.feed_timeout
);
//! SIGINT handle registered by `scanner` constructor
lws::scanner scanner{disk.clone(), prog.rest_config.webhook_verify};
std::shared_ptr<lws::mempool> mempool;
MINFO("Using monerod ZMQ RPC at " << ctx.daemon_address());
if (!sub_address.empty())
{
MINFO("Using monerod ZMQ sub at " << sub_address);
mempool = std::make_shared<lws::mempool>();
}
auto client = scanner.sync(ctx.connect().value(), prog.untrusted_daemon).value();
lws::rest_server server{
epee::to_span(prog.rest_servers), prog.admin_rest_servers, std::move(disk), std::move(client), std::move(prog.rest_config)
epee::to_span(prog.rest_servers),
prog.admin_rest_servers,
std::move(disk),
std::move(client),
mempool,
std::move(prog.rest_config)
};
for (const std::string& address : prog.rest_servers)
MINFO("Listening for REST clients at " << address);
@@ -378,6 +400,7 @@ namespace
// blocks until SIGINT
scanner.run(
std::move(ctx),
mempool,
prog.scan_threads,
std::move(prog.lws_server_addr),
std::move(prog.lws_server_pass),
+2 -2
View File
@@ -27,8 +27,8 @@
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
set(wownero-lws-util_sources blocks.cpp gamma_picker.cpp random_outputs.cpp source_location.cpp transactions.cpp)
set(wownero-lws-util_headers blocks.h fwd.h gamma_picker.h random_outputs.h source_location.h transactions.h)
set(wownero-lws-util_sources blocks.cpp gamma_picker.cpp ownership_test.cpp random_outputs.cpp source_location.cpp transactions.cpp)
set(wownero-lws-util_headers blocks.h fwd.h gamma_picker.h ownership_test.h random_outputs.h source_location.h transactions.h)
add_library(wownero-lws-util ${wownero-lws-util_sources} ${wownero-lws-util_headers})
target_link_libraries(wownero-lws-util wownero::libraries wownero-lws-db)
+339
View File
@@ -0,0 +1,339 @@
// Copyright (c) 2018-2025, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "ownership_test.h"
#include <boost/optional/optional.hpp>
#include <boost/range/combine.hpp>
#include "common/error.h"
#include "crypto/wallet/crypto.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "cryptonote_basic/tx_extra.h"
#include "db/account.h"
#include "db/data.h"
#include "db/storage.h"
#include "error.h"
#include "misc_log_ex.h"
#include "rpc/daemon_messages.h"
#include "util/transactions.h"
namespace lws
{
ownership_test::ownership_test(spend_action on_spend, output_action on_output)
: on_spend(std::move(on_spend))
, on_output(std::move(on_output))
{}
void ownership_test::enable_subaddresses(const db::storage& disk, const std::uint32_t max_subaddresses)
{
subaddress.emplace(disk, max_subaddresses);
}
void ownership_test::disable_subaddresses()
{
subaddress.reset();
}
void ownership_test::operator()(
epee::span<account> users,
db::block_id height,
std::uint64_t timestamp,
crypto::hash const* tx_hash,
const cryptonote::transaction& tx,
const std::vector<std::uint64_t>& out_ids)
{
if (2 < tx.version)
throw std::runtime_error{"Unsupported tx version"};
crypto::hash tx_hash_lazy;
cryptonote::tx_extra_pub_key key;
boost::optional<crypto::hash> prefix_hash;
boost::optional<cryptonote::tx_extra_nonce> extra_nonce;
std::pair<std::uint8_t, db::output::payment_id_> payment_id;
cryptonote::tx_extra_additional_pub_keys additional_tx_pub_keys;
std::vector<crypto::key_derivation> additional_derivations;
const auto get_tx_hash = [&] () -> const crypto::hash&
{
if (!tx_hash)
{
tx_hash_lazy = get_transaction_hash(tx);
tx_hash = std::addressof(tx_hash_lazy);
}
return *tx_hash;
};
{
std::vector<cryptonote::tx_extra_field> extra;
cryptonote::parse_tx_extra(tx.extra, extra);
if (!cryptonote::find_tx_extra_field_by_type(extra, key))
return;
extra_nonce.emplace();
if (cryptonote::find_tx_extra_field_by_type(extra, *extra_nonce))
{
if (cryptonote::get_payment_id_from_tx_extra_nonce(extra_nonce->nonce, payment_id.second.long_))
payment_id.first = sizeof(crypto::hash);
}
else
extra_nonce = boost::none;
if (subaddress)
cryptonote::find_tx_extra_field_by_type(extra, additional_tx_pub_keys);
}
for (account& user : users)
{
if (height <= user.scan_height())
continue;
crypto::key_derivation derived;
if (!crypto::wallet::generate_key_derivation(key.pub_key, user.view_key(), derived))
continue;
if (subaddress && additional_tx_pub_keys.data.size() == tx.vout.size())
{
additional_derivations.resize(tx.vout.size());
for (std::size_t index = 0; index < tx.vout.size(); ++index)
{
if (!crypto::wallet::generate_key_derivation(additional_tx_pub_keys.data[index], user.view_key(), additional_derivations[index]))
{
additional_derivations.clear();
break;
}
}
}
db::extra ext{};
std::uint32_t mixin = 0;
for (auto const& in : tx.vin)
{
if (const auto in_data = boost::get<cryptonote::txin_to_key>(std::addressof(in)))
{
mixin = boost::numeric_cast<std::uint32_t>(std::max<std::size_t>(1, in_data->key_offsets.size()) - 1);
std::uint64_t goffset = 0;
for (std::uint64_t offset : in_data->key_offsets)
{
goffset += offset;
const auto address_index = user.get_spendable(db::output_id{in_data->amount, goffset});
if (!address_index)
continue;
on_spend(
user,
db::spend{
db::transaction_link{height, get_tx_hash()},
in_data->k_image,
db::output_id{in_data->amount, goffset},
timestamp,
tx.unlock_time,
mixin,
{0, 0, 0},
payment_id.first,
payment_id.second.long_,
*address_index
}
);
}
}
else if (boost::get<cryptonote::txin_gen>(std::addressof(in)))
ext = db::extra(ext | db::coinbase_output);
}
for (std::size_t index = 0; index < tx.vout.size(); ++index)
{
crypto::public_key out_pub_key;
if (!cryptonote::get_output_public_key(tx.vout[index], out_pub_key))
continue;
const auto view_tag = cryptonote::get_output_view_tag(tx.vout[index]);
const bool matched =
(!additional_derivations.empty() && cryptonote::out_can_be_to_acc(view_tag, additional_derivations.at(index), index)) ||
cryptonote::out_can_be_to_acc(view_tag, derived, index);
if (!matched)
continue;
bool found_pub = false;
db::address_index account_index{db::major_index::primary, db::minor_index::primary};
crypto::key_derivation active_derivation{};
crypto::public_key active_pub{};
for (std::size_t attempt = 0; attempt < 2; ++attempt)
{
if (attempt == 0)
{
active_derivation = derived;
active_pub = key.pub_key;
}
else if (!additional_derivations.empty())
{
active_derivation = additional_derivations.at(index);
active_pub = additional_tx_pub_keys.data.at(index);
}
else
break;
crypto::public_key derived_pub;
if (!crypto::wallet::derive_subaddress_public_key(out_pub_key, active_derivation, index, derived_pub))
continue;
if (user.spend_public() != derived_pub)
{
if (!subaddress)
continue;
const expect<db::address_index> match = subaddress->find_subaddress(user, derived_pub);
if (!match)
{
if (match != lmdb::error(MDB_NOTFOUND))
MERROR("Failure when doing subaddress search: " << match.error().message());
continue;
}
auto result = subaddress->update_lookahead(user, *match, height);
if (!result)
MWARNING("Failed to update lookahead for " << user.address() << ": " << result.error());
found_pub = true;
account_index = *match;
break;
}
else
{
found_pub = true;
break;
}
}
if (!found_pub)
continue;
if (!prefix_hash)
{
prefix_hash.emplace();
cryptonote::get_transaction_prefix_hash(tx, *prefix_hash);
}
std::uint64_t amount = tx.vout[index].amount;
rct::key mask = rct::identity();
if (!amount && !(ext & db::coinbase_output) && 1 < tx.version)
{
const bool bulletproof2 = (rct::RCTTypeBulletproof2 <= tx.rct_signatures.type);
const auto decrypted = lws::decode_amount(
tx.rct_signatures.outPk.at(index).mask,
tx.rct_signatures.ecdhInfo.at(index),
active_derivation,
index,
bulletproof2,
tx.rct_signatures.type
);
if (!decrypted)
{
MWARNING(user.address() << " failed to decrypt amount for tx " << get_tx_hash() << ", skipping output");
continue;
}
amount = decrypted->first;
mask = decrypted->second;
ext = db::extra(ext | db::ringct_output);
}
else if (1 < tx.version)
ext = db::extra(ext | db::ringct_output);
if (extra_nonce && !payment_id.first && cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce->nonce, payment_id.second.short_))
{
payment_id.first = sizeof(crypto::hash8);
lws::decrypt_payment_id(payment_id.second.short_, active_derivation);
}
on_output(
user,
db::output{
db::transaction_link{height, get_tx_hash()},
db::output::spend_meta_{
db::output_id{tx.version < 2 ? tx.vout[index].amount : 0, out_ids.at(index)},
amount,
mixin,
boost::numeric_cast<std::uint32_t>(index),
active_pub
},
timestamp,
tx.unlock_time,
*prefix_hash,
out_pub_key,
mask,
{0, 0, 0, 0, 0, 0, 0},
db::pack(ext, payment_id.first),
payment_id.second,
cryptonote::get_tx_fee(tx),
account_index
}
);
}
}
}
subaddress_reader::subaddress_reader(db::storage const& disk_in, const std::uint32_t max_subaddresses)
: reader(common_error::kInvalidArgument), disk(disk_in.clone()), cur(nullptr), max_subaddresses(max_subaddresses)
{
update_reader();
}
void subaddress_reader::update_reader()
{
reader = disk.start_read();
if (!reader)
MERROR("Subadress lookup failure: " << reader.error().message());
}
expect<db::address_index> subaddress_reader::find_subaddress(const account& user, crypto::public_key const& pubkey)
{
if (!reader)
return {lmdb::error(MDB_NOTFOUND)};
return reader->find_subaddress(user.id(), pubkey, cur);
}
expect<void> subaddress_reader::update_lookahead(const account& user, const db::address_index& match, db::block_id height)
{
if (match.is_zero())
return {}; // keep subaddress disabled servers quick
if (height == db::block_id::txpool)
height = user.scan_height();
auto upserted = disk.update_lookahead(user.db_address(), height, match, max_subaddresses);
if (!upserted)
return upserted.error();
if (0 < *upserted)
update_reader(); // update reader after upsert added new addresses
else if (*upserted < 0)
upserted = {error::max_subaddresses};
return upserted.error();
}
} // namespace lws
+95
View File
@@ -0,0 +1,95 @@
// Copyright (c) 2018-2025, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <boost/optional/optional.hpp>
#include <functional>
#include "cryptonote_basic/cryptonote_basic.h"
#include "db/account.h"
#include "db/storage.h"
#include "span.h"
namespace lws
{
class subaddress_reader
{
public:
subaddress_reader(db::storage const& disk_in, const std::uint32_t max_subaddresses);
expect<db::address_index> find_subaddress(const account& user, crypto::public_key const& pubkey);
expect<void> update_lookahead(const account& user, const db::address_index& match, db::block_id height);
private:
expect<db::storage_reader> reader;
db::storage disk;
db::cursor::subaddress_indexes cur;
const std::uint32_t max_subaddresses;
void update_reader();
};
class ownership_test {
public:
using spend_action = std::function<void(account&, db::spend&&)>;
using output_action = std::function<void(account&, db::output&&)>;
ownership_test(spend_action, output_action);
ownership_test(const ownership_test&) = delete;
ownership_test(ownership_test&&) = default;
ownership_test& operator=(const ownership_test&) = delete;
ownership_test& operator=(ownership_test&&) = default;
/*! Tests the transaction against our accounts,
and invokes callbacks for matching inputs or outputs.
@param height mined height
@param timestamp mined block timestamp
@param out_ids maps vout indices to global utxo indexes
*/
void operator()(
epee::span<account> users,
db::block_id height,
std::uint64_t timestamp,
crypto::hash const* tx_hash,
const cryptonote::transaction& tx,
const std::vector<std::uint64_t>& out_ids
);
void enable_subaddresses(const db::storage& disk, const std::uint32_t max_subaddresses);
void disable_subaddresses();
private:
spend_action on_spend;
output_action on_output;
boost::optional<subaddress_reader> subaddress;
};
} // namespace lws
+3 -1
View File
@@ -34,9 +34,10 @@ target_link_libraries(wownero-lws-unit-framework)
add_subdirectory(db)
add_subdirectory(net)
add_subdirectory(rpc)
add_subdirectory(util)
add_subdirectory(wire)
add_executable(wownero-lws-unit main.cpp rest.test.cpp scanner.test.cpp)
add_executable(wownero-lws-unit main.cpp mempool.test.cpp rest.test.cpp scanner.test.cpp)
target_link_libraries(wownero-lws-unit
wownero::libraries
wownero-lws-daemon-common
@@ -45,6 +46,7 @@ target_link_libraries(wownero-lws-unit
wownero-lws-unit-net
wownero-lws-unit-net-http
wownero-lws-unit-rpc
wownero-lws-unit-util
wownero-lws-unit-wire
wownero-lws-unit-wire-json
wownero-lws-unit-wire-msgpack
+38 -1
View File
@@ -152,6 +152,43 @@ LWS_CASE("db::storage::*_webhook")
EXPECT(result.empty());
}
SECTION("webhooks with empty outs")
{
const boost::uuids::uuid id = boost::uuids::random_generator{}();
const boost::uuids::uuid id2 = boost::uuids::random_generator{}();
{
lws::db::webhook_value value{
lws::db::webhook_dupsort{0, id},
lws::db::webhook_data{"http://the_url_spend", "the_token_spend"}
};
lws::db::webhook_value value2{
lws::db::webhook_dupsort{0, id2},
lws::db::webhook_data{"http://the_url_spend", "the_token_spend"}
};
MONERO_UNWRAP(
db.add_webhook(lws::db::webhook_type::tx_spend, account, std::move(value))
);
MONERO_UNWRAP(
db.add_webhook(lws::db::webhook_type::tx_confirmation, account, std::move(value2))
);
}
const auto scan_height = lws::db::block_id(lmdb::to_native(last_block.id) + 1);
crypto::hash chain[2] = {
last_block.hash,
crypto::rand<crypto::hash>()
};
lws::account full_account = lws::db::test::make_account(account, view);
full_account.updated(last_block.id);
auto updated = db.update(last_block.id, chain, {std::addressof(full_account), 1}, nullptr);
EXPECT(updated.has_value());
EXPECT(updated->spend_pubs.empty());
EXPECT(updated->accounts_updated == 1);
EXPECT(updated->confirm_pubs.size() == 0);
}
SECTION("storage::update(...) one at a time")
{
lws::account full_account = lws::db::test::make_account(account, view);
@@ -436,7 +473,7 @@ LWS_CASE("db::storage::*_webhook")
EXPECT(spends.size() == 1);
const auto updated = db.update(last_block.id, chain, {std::addressof(full_account), 1}, nullptr);
EXPECT(!updated.has_error());
EXPECT(updated.error() == std::error_code{});
EXPECT(updated->accounts_updated == 1);
EXPECT(updated->confirm_pubs.size() == 3);
EXPECT(updated->spend_pubs.size() == 1);
+137
View File
@@ -0,0 +1,137 @@
// Copyright (c) 2024, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "framework.test.h"
#include <cstring>
#include "cryptonote_basic/cryptonote_format_utils.h" // monero/src
#include "cryptonote_core/cryptonote_tx_utils.h" // monero/src
#include "db/account.h"
#include "db/data.h"
#include "mempool.h"
#include "util/account.test.h"
#include "util/transaction.test.h"
namespace
{
lws::db::account make_account(const cryptonote::account_keys& src)
{
lws::db::account out{};
std::memcpy(&out.address.spend_public, &src.m_account_address.m_spend_public_key, sizeof(out.address.spend_public));
std::memcpy(&out.address.view_public, &src.m_account_address.m_view_public_key, sizeof(out.address.view_public));
std::memcpy(&out.key, &unwrap(unwrap(src.m_view_secret_key)), sizeof(out.key));
return out;
}
}
LWS_CASE("mempool")
{
const auto base = lws_test::make_account();
SETUP("mempool")
{
lws::mempool pool{};
lws::account account{make_account(base), {}, {}};
SECTION("empty")
{
EXPECT(pool.scan_account(account).empty());
}
SECTION("add_txs")
{
const auto other = lws_test::make_account();
std::vector<cryptonote::tx_destination_entry> destinations;
destinations.emplace_back();
destinations.back().amount = 8000;
destinations.back().addr = base.m_account_address;
const auto tx1 = lws_test::make_tx(lest_env, base, destinations, 20, true);
destinations.back().amount = 9000;
destinations.back().addr = other.m_account_address;
const auto tx2 = lws_test::make_tx(lest_env, other, destinations, 20, true);
std::array<cryptonote::transaction, 2> txes{{tx1.tx, tx2.tx}};
pool.add_txs(epee::to_mut_span(txes));
auto results = pool.scan_account(account);
EXPECT(results.size() == 1);
EXPECT(results.at(0).hash == get_transaction_hash(tx1.tx));
EXPECT(results.at(0).spends.size() == 0);
EXPECT(results.at(0).outputs.size() == 1);
EXPECT(results.at(0).outputs.at(0).link.height == lws::db::block_id::txpool);
EXPECT(results.at(0).outputs.at(0).link.tx_hash == get_transaction_hash(tx1.tx));
results = pool.scan_account(account);
EXPECT(results.size() == 1);
EXPECT(results.at(0).hash == get_transaction_hash(tx1.tx));
EXPECT(results.at(0).spends.size() == 0);
EXPECT(results.at(0).outputs.size() == 1);
EXPECT(results.at(0).outputs.at(0).link.height == lws::db::block_id::txpool);
EXPECT(results.at(0).outputs.at(0).link.tx_hash == get_transaction_hash(tx1.tx));
pool.reset_txs(lws::mempool::pool_table{});
results = pool.scan_account(account);
EXPECT(results.empty());
}
SECTION("rest_txs")
{
const auto now = std::chrono::system_clock::now();
const auto other = lws_test::make_account();
std::vector<cryptonote::tx_destination_entry> destinations;
destinations.emplace_back();
destinations.back().amount = 8000;
destinations.back().addr = base.m_account_address;
const auto tx1 = lws_test::make_tx(lest_env, base, destinations, 20, true);
destinations.back().amount = 9000;
destinations.back().addr = other.m_account_address;
const auto tx2 = lws_test::make_tx(lest_env, other, destinations, 20, true);
pool.reset_txs(
lws::mempool::pool_table{
{get_transaction_hash(tx1.tx), std::make_shared<lws::mempool::pool_entry>(cryptonote::transaction{tx1.tx}, now)},
{get_transaction_hash(tx2.tx), std::make_shared<lws::mempool::pool_entry>(cryptonote::transaction{tx2.tx}, now)}
}
);
auto results = pool.scan_account(account);
EXPECT(results.size() == 1);
EXPECT(results.at(0).hash == get_transaction_hash(tx1.tx));
EXPECT(results.at(0).spends.size() == 0);
EXPECT(results.at(0).outputs.size() == 1);
EXPECT(results.at(0).outputs.at(0).link.height == lws::db::block_id::txpool);
EXPECT(results.at(0).outputs.at(0).link.tx_hash == get_transaction_hash(tx1.tx));
}
}
}
+565 -47
View File
@@ -27,16 +27,42 @@
#include "framework.test.h"
#include <boost/asio/ip/tcp.hpp>
#include <boost/beast/websocket/error.hpp>
#include <memory>
#include <optional>
#include <time.h>
#include "cryptonote_basic/account.h" // monero/src
#include "cryptonote_core/cryptonote_tx_utils.h" // monero/src
#include "db/data.h"
#include "db/print.test.h"
#include "db/storage.test.h"
#include "db/string.h"
#include "error.h"
#include "hex.h" // monero/epee/contrib/include
#include "mempool.h"
#include "net/http_client.h"
#include "rapidjson/document.h" // monero/external/rapidjson/include
#include "rapidjson/stringbuffer.h" // monero/external/rapidjson/incldue
#include "rapidjson/prettywriter.h" // monero/external/rapidjson/incldue
#include "rest_server.h"
#include "rpc/pull.test.h"
#include "scanner.test.h"
#include "util/account.test.h"
#include "util/transaction.test.h"
namespace rapidjson
{
std::ostream& operator<<(std::ostream& out, const Document& src)
{
StringBuffer buffer;
PrettyWriter<StringBuffer> writer{buffer};
src.Accept(writer);
return out << buffer.GetString();
}
}
namespace
{
@@ -59,14 +85,20 @@ namespace
rct::key amount;
};
std::string invoke(enet::http::http_simple_client& client, const boost::string_ref uri, const boost::string_ref body)
std::pair<std::string, unsigned> invoke_base(enet::http::http_simple_client& client, const boost::string_ref uri, const boost::string_ref body)
{
const enet::http::http_response_info* info = nullptr;
if (!client.invoke(uri, "POST", body, std::chrono::milliseconds{500}, std::addressof(info), {}))
throw std::runtime_error{"HTTP invoke failed"};
if (info->m_response_code != 200)
throw std::runtime_error{"HTTP invoke not 200, instead " + std::to_string(info->m_response_code)};
return std::string{info->m_body};
return {std::string{info->m_body}, info->m_response_code};
}
std::string invoke(enet::http::http_simple_client& client, const boost::string_ref uri, const boost::string_ref body)
{
auto result = invoke_base(client, uri, body);
if (result.second != 200)
throw std::runtime_error{"HTTP invoke not 200, instead " + std::to_string(result.second)};
return result.first;
}
epee::byte_slice get_fee_response()
@@ -105,14 +137,40 @@ namespace
return out;
}
void verify_json(lest::env& lest_env, std::string name, std::string actual_json, std::string expected_json)
{
const lest::ctx ctx{lest_env, std::move(name)};
rapidjson::Document expected;
expected.Parse(expected_json.c_str());
EXPECT(!expected.HasParseError());
rapidjson::Document actual;
actual.Parse(actual_json.c_str());
EXPECT(!actual.HasParseError());
EXPECT(expected == actual);
}
std::string get_prefix(const std::string& raw)
{
char const* const sep = std::strchr(raw.c_str(), u8':');
if (!sep)
return {};
return {raw.data(), std::size_t(sep - raw.data() + 1)};
}
}
LWS_CASE("rest_server")
{
lws::db::account_address account_address{};
crypto::secret_key view{};
crypto::generate_keys(account_address.spend_public, view);
crypto::generate_keys(account_address.view_public, view);
const auto now = std::chrono::system_clock::now();
const auto base = lws_test::make_account();
lws::db::account_address account_address{
base.m_account_address.m_view_public_key,
base.m_account_address.m_spend_public_key
};
const crypto::secret_key view = base.m_view_secret_key;
const std::string address = lws::db::address_string(account_address);
const std::string viewkey = epee::to_hex::string(epee::as_byte_span(unwrap(unwrap(view))));
@@ -122,11 +180,12 @@ LWS_CASE("rest_server")
lws::db::test::cleanup_db on_scope_exit{};
lws::db::storage db = lws::db::test::get_fresh_db();
auto context =
lws::rpc::context::make(lws_test::rpc_rendevous, {}, {}, {}, std::chrono::minutes{0}, false);
lws::rpc::context::make(lws_test::rpc_rendevous, {}, {}, {}, std::chrono::minutes{0}, false, true);
const auto rpc = MONERO_UNWRAP(context.connect());
const auto init_server = [&] (std::shared_ptr<lws::mempool> pool)
{
const lws::rest_server::configuration config{
{}, {}, 1, 20, {}, false, true, true
{}, {}, std::chrono::seconds{10}, 1, 20, {}, false, true, true, false
};
std::vector<std::string> addresses{rest_server};
server.emplace(
@@ -134,9 +193,12 @@ LWS_CASE("rest_server")
std::vector<std::string>{admin_server},
db.clone(),
MONERO_UNWRAP(rpc.clone()),
std::move(pool),
config
);
}
};
init_server(nullptr);
const lws::db::block_info last_block =
MONERO_UNWRAP(MONERO_UNWRAP(db.start_read()).get_last_block());
@@ -145,6 +207,13 @@ LWS_CASE("rest_server")
return MONERO_UNWRAP(MONERO_UNWRAP(db.start_read()).get_account(account_address)).second;
};
const auto get_full_account = [&db, &get_account] () -> lws::account
{
const lws::db::account acct = get_account();
return MONERO_UNWRAP(MONERO_UNWRAP(db.start_read()).get_full_account(acct));
};
enet::http::http_simple_client client{};
client.set_server("127.0.0.1", "10000", boost::none);
EXPECT(client.connect(std::chrono::milliseconds{500}));
@@ -534,8 +603,30 @@ LWS_CASE("rest_server")
);
}
SECTION("One Receive, One Spend")
SECTION("One Receive, One Spend, and one mempool")
{
const auto pool = std::make_shared<lws::mempool>();
EXPECT(client.disconnect());
init_server(pool);
EXPECT(client.connect(std::chrono::milliseconds{500}));
lws_test::transaction tx;
{
std::vector<cryptonote::tx_destination_entry> destinations;
destinations.emplace_back();
destinations.back().amount = 8000;
destinations.back().addr = base.m_account_address;
tx = lws_test::make_tx(lest_env, base, destinations, 20, true);
const auto hash = get_transaction_hash(tx.tx);
pool->reset_txs(
lws::mempool::pool_table{
{hash, std::make_shared<lws::mempool::pool_entry>(cryptonote::transaction{tx.tx}, now)}
}
);
}
const std::string scan_height = std::to_string(std::uint64_t(account.scan_height) + 5);
const std::string start_height = std::to_string(std::uint64_t(account.start_height));
message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\"}";
@@ -563,7 +654,7 @@ LWS_CASE("rest_server")
lws::db::output{
link,
lws::db::output::spend_meta_{
lws::db::output_id{500, 30},
lws::db::output_id{0, 35},
std::uint64_t(40000),
std::uint32_t(16),
std::uint32_t(2),
@@ -585,7 +676,7 @@ LWS_CASE("rest_server")
lws::db::spend{
link,
image,
lws::db::output_id{500, 30},
lws::db::output_id{0, 35},
std::uint64_t(66),
std::uint64_t(1500),
std::uint32_t(16),
@@ -630,37 +721,61 @@ LWS_CASE("rest_server")
);
response = invoke(client, "/get_address_txs", message);
EXPECT(response ==
"{\"total_received\":\"40000\","
"\"scanned_height\":" + scan_height + "," +
"\"scanned_block_height\":" + scan_height + ","
"\"start_height\":" + start_height + ","
"\"transaction_height\":" + scan_height + ","
"\"blockchain_height\":" + scan_height + ","
"\"transactions\":["
"{\"id\":0,"
"\"hash\":\"" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + "\","
"\"timestamp\":\"1970-01-01T01:56:40Z\","
"\"total_received\":\"40000\","
"\"total_sent\":\"40000\","
"\"fee\":\"100\","
"\"unlock_time\":4670,"
"\"height\":4000,"
"\"payment_id\":\"" + epee::to_hex::string(epee::as_byte_span(payment_id_.long_)) + "\","
"\"coinbase\":true,"
"\"mempool\":false,"
"\"mixin\":16,"
"\"recipient\":{\"maj_i\":2,\"min_i\":66},"
"\"spent_outputs\":[{"
"\"amount\":\"40000\","
"\"key_image\":\"" + epee::to_hex::string(epee::as_byte_span(image)) + "\","
"\"tx_pub_key\":\"" + epee::to_hex::string(epee::as_byte_span(tx_public)) + "\","
"\"out_index\":2,"
"\"mixin\":16,"
"\"sender\":{\"maj_i\":4,\"min_i\":55}"
"}]}"
"]}"
);
const std::string expected =
R"({
"total_received":"48000",
"scanned_height":)" + scan_height + R"(,
"scanned_block_height":)" + scan_height + R"(,
"start_height":)" + start_height + R"(,
"transaction_height":)" + scan_height + R"(,
"blockchain_height":)" + scan_height + R"(,
"transactions":[
{
"id":0,
"hash":")" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + R"(",
"timestamp":"1970-01-01T01:56:40Z",
"total_received":"40000",
"total_sent":"40000",
"fee":"100",
"unlock_time":4670,
"height":4000,
"payment_id":")" + epee::to_hex::string(epee::as_byte_span(payment_id_.long_)) + R"(",
"coinbase":true,
"mempool":false,
"mixin":16,
"recipient":{"maj_i":2,"min_i":66},
"spent_outputs":[{
"amount":"40000",
"key_image":")" + epee::to_hex::string(epee::as_byte_span(image)) + R"(",
"tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx_public)) + R"(",
"out_index":2,
"mixin":16,
"sender":{"maj_i":4,"min_i":55}
}]
},{
"id":1,
"hash":")" + epee::to_hex::string(epee::as_byte_span(get_transaction_hash(tx.tx))) + R"(",
"total_received":"8000",
"total_sent":"40000",
"fee":"12000",
"unlock_time":0,
"payment_id":"0000000000000000",
"coinbase":false,
"mempool":true,
"mixin":15,
"recipient":{"maj_i":0,"min_i":0},
"spent_outputs":[{
"amount":"40000",
"key_image":")" + epee::to_hex::string(epee::as_byte_span(tx.images.at(0))) + R"(",
"tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx_public)) + R"(",
"out_index":2,
"mixin":15,
"sender":{"maj_i":2, "min_i": 66}
}]
}
]
})";
verify_json(lest_env, "/get_address_txs", response, expected);
std::vector<epee::byte_slice> messages;
messages.emplace_back(get_fee_response());
@@ -678,8 +793,8 @@ LWS_CASE("rest_server")
"{\"amount\":\"40000\","
"\"public_key\":\"" + epee::to_hex::string(epee::as_byte_span(pub)) + "\","
"\"index\":2,"
"\"global_index\":30,"
"\"tx_id\":30,"
"\"global_index\":35,"
"\"tx_id\":35,"
"\"tx_hash\":\"" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + "\","
"\"tx_prefix_hash\":\"" + epee::to_hex::string(epee::as_byte_span(tx_prefix)) + "\","
"\"tx_pub_key\":\"" + epee::to_hex::string(epee::as_byte_span(tx_public)) + "\","
@@ -745,6 +860,409 @@ LWS_CASE("rest_server")
"{\"key\":0,\"value\":[[1,20]]}]}"
);
}
SECTION("feed check")
{
auto response = invoke_base(client, "/feed", "");
EXPECT(response.second == 501);
EXPECT(response.first == "");
}
SECTION("feed subscription")
{
const auto pool = std::make_shared<lws::mempool>();
const std::string scan_height = std::to_string(std::uint64_t(account.scan_height));
namespace pull = lws_test::rpc::pull;
const auto local = boost::asio::ip::make_address("127.0.0.1");
boost::asio::io_context io;
boost::asio::steady_timer timeout{io};
timeout.expires_after(std::chrono::seconds{5});
timeout.async_wait([&io] (auto) { io.stop(); });
//
// invalid version
//
std::shared_ptr<pull::connection> conn =
pull::make(io, boost::asio::ip::tcp::endpoint(local, 10000), {});
bool ran = false;
boost::system::error_code error{};
std::string response;
message = R"(login:{"account":{"address":")" + address + R"(","view_key":")" + viewkey + R"("}})";
const auto handler = [&] (auto ec, auto msg) { ran = true; error = ec; response = msg; };
pull::async_handshake(conn, message, handler);
while (!ran && !io.stopped())
{
io.restart();
io.run_one();
}
EXPECT(!io.stopped());
EXPECT(error == boost::beast::websocket::error::upgrade_declined);
//
// empty tx sync
//
conn = pull::make(io, boost::asio::ip::tcp::endpoint(local, 10000), "lws.feed.v0.json");
pull::async_handshake(conn, message, handler);
ran = false;
error = {};
response = {};
while (!ran && !io.stopped())
{
io.restart();
io.run_one();
}
std::string expected=
R"({"scanned_block_height":)" + scan_height + R"(,
"start_height":)" + scan_height + R"(,
"blockchain_height":)" + scan_height + "}";
EXPECT(!io.stopped());
EXPECT(error == boost::system::error_code{});
EXPECT(get_prefix(response) == "tx_sync:");
response.erase(0, std::strlen("tx_sync:"));
verify_json(lest_env, "tx_sync", response, expected);
pull::async_close(conn, handler);
ran = false;
error = {};
response = {};
while (!ran && !io.stopped())
{
io.restart();
io.run_one();
}
EXPECT(!io.stopped());
EXPECT(error == boost::system::error_code{});
EXPECT(response == "");
// mempool tx in sync//
init_server(pool);
conn = pull::make(io, boost::asio::ip::tcp::endpoint(local, 10000), "lws.feed.v0.json");
pull::async_handshake(conn, message, handler);
lws_test::transaction tx;
{
std::vector<cryptonote::tx_destination_entry> destinations;
destinations.emplace_back();
destinations.back().amount = 8000;
destinations.back().addr = base.m_account_address;
tx = lws_test::make_tx(lest_env, base, destinations, 20, true);
const auto hash = get_transaction_hash(tx.tx);
pool->reset_txs(
lws::mempool::pool_table{
{hash, std::make_shared<lws::mempool::pool_entry>(cryptonote::transaction{tx.tx}, now)}
}
);
}
ran = false;
error = {};
response = {};
while (!ran && !io.stopped())
{
io.restart();
io.run_one();
}
expected =
R"({"scanned_block_height":)" + scan_height + R"(,
"start_height":)" + scan_height + R"(,
"blockchain_height":)" + scan_height + R"(,
"transactions":[{
"hash":")" + epee::to_hex::string(epee::as_byte_span(get_transaction_hash(tx.tx))) + R"(",
"prefix_hash":")" + epee::to_hex::string(epee::as_byte_span(get_transaction_prefix_hash(tx.tx))) + R"(",
"timestamp":)" + std::to_string(std::chrono::system_clock::to_time_t(now)) + R"(,
"fee":12000,
"unlock_time":0,
"payment_id": "0000000000000000",
"mempool":true,
"mixin":15,
"receives":[
{
"amount":8000,
"public_key":")" + epee::to_hex::string(epee::as_byte_span(tx.spend_publics.at(0))) + R"(",
"index":0,
"tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx.pub_keys.at(0))) + R"(",
"rct":")" + epee::to_hex::string(epee::as_byte_span(tx.ringct.at(0))) + R"("
}
]
}]
})";
EXPECT(!io.stopped());
EXPECT(error == boost::system::error_code{});
EXPECT(get_prefix(response) == "tx_sync:");
response.erase(0, std::strlen("tx_sync:"));
verify_json(lest_env, "tx_sync non empty", response, expected);
const lws::db::transaction_link link{
lws::db::block_id::txpool, crypto::rand<crypto::hash>()
};
const crypto::public_key tx_public = []() {
crypto::secret_key secret;
crypto::public_key out;
crypto::generate_keys(out, secret);
return out;
}();
const crypto::hash tx_prefix = crypto::rand<crypto::hash>();
const crypto::public_key pub = crypto::rand<crypto::public_key>();
const crypto::public_key pub2 = crypto::rand<crypto::public_key>();
const rct::key ringct = crypto::rand<rct::key>();
const auto extra = lws::db::extra(lws::db::extra::ringct_output);
const auto payment_id_ = crypto::rand<lws::db::output::payment_id_>();
rpc.push_update(
get_full_account(),
lws::mempool_receive{
lws::db::output{
link,
lws::db::output::spend_meta_{
lws::db::output_id::txpool(),
std::uint64_t(40000),
std::uint32_t(16),
std::uint32_t(2),
tx_public
},
std::uint64_t(7000),
std::uint64_t(4670),
tx_prefix,
pub,
ringct,
{0, 0, 0, 0, 0, 0, 0},
lws::db::pack(extra, sizeof(crypto::hash)),
payment_id_,
std::uint64_t(100),
lws::db::address_index{lws::db::major_index(2), lws::db::minor_index(66)}
}
}
);
pull::async_next_message(conn, handler);
ran = false;
error = {};
response = {};
while (!ran && !io.stopped())
{
io.restart();
io.run_one();
}
expected =
R"({
"hash":")" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + R"(",
"prefix_hash":")" + epee::to_hex::string(epee::as_byte_span(tx_prefix)) + R"(",
"fee":100,
"unlock_time":4670,
"payment_id":")" + epee::to_hex::string(epee::as_byte_span(payment_id_.long_)) + R"(",
"mixin":16,
"amount":40000,
"public_key":")" + epee::to_hex::string(epee::as_byte_span(pub)) + R"(",
"index":2,
"tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx_public)) + R"(",
"rct":")" + epee::to_hex::string(epee::as_byte_span(ringct)) + R"(",
"recipient":{"maj_i":2,"min_i":66}
})";
EXPECT(!io.stopped());
EXPECT(error == boost::system::error_code{});
EXPECT(get_prefix(response) == "mempool:");
response.erase(0, std::strlen("mempool:"));
verify_json(lest_env, "mempool", response, expected);
const lws::db::block_id new_height = lws::db::block_id(to_uint(last_block.id) + 1);
const crypto::key_image image = crypto::rand<crypto::key_image>();
{
lws::account account = get_full_account();
account.add_out(
lws::db::output{
{new_height, link.tx_hash},
lws::db::output::spend_meta_{
lws::db::output_id{0, 100},
std::uint64_t(40000),
std::uint32_t(16),
std::uint32_t(2),
tx_public
},
std::uint64_t(7000),
std::uint64_t(4670),
tx_prefix,
pub,
ringct,
{0, 0, 0, 0, 0, 0, 0},
lws::db::pack(extra, sizeof(crypto::hash)),
payment_id_,
std::uint64_t(100),
lws::db::address_index{lws::db::major_index(2), lws::db::minor_index(66)}
}
);
account.add_out(
lws::db::output{
{new_height, link.tx_hash},
lws::db::output::spend_meta_{
lws::db::output_id{0, 99},
std::uint64_t(30000),
std::uint32_t(16),
std::uint32_t(1),
tx_public
},
std::uint64_t(7000),
std::uint64_t(4670),
tx_prefix,
pub2,
ringct,
{0, 0, 0, 0, 0, 0, 0},
lws::db::pack(extra, sizeof(crypto::hash)),
payment_id_,
std::uint64_t(100),
lws::db::address_index{lws::db::major_index(0), lws::db::minor_index(0)}
}
);
account.add_spend(
lws::db::spend{
{new_height, link.tx_hash},
image,
lws::db::output_id{0, 90},
std::uint64_t(7000),
std::uint64_t(0),
std::uint16_t(16),
{0, 0, 0},
std::uint8_t(0),
{},
{}
}
);
const std::array<crypto::hash, 2> chain{{
last_block.hash, crypto::rand<crypto::hash>()
}};
MONERO_UNWRAP(db.update(last_block.id, epee::to_span(chain), {std::addressof(account), 1}, nullptr));
rpc.push_updates({std::addressof(account), 1}, new_height);
}
pull::async_next_message(conn, handler);
ran = false;
error = {};
response = {};
while (!ran && !io.stopped())
{
io.restart();
io.run_one();
}
std::string new_height_str = std::to_string(to_uint(new_height));
expected =
R"({
"scan_start":)" + scan_height + R"(,
"scan_end":)" + new_height_str + R"(,
"blockchain_height":)" + new_height_str + R"(,
"transactions":[{
"hash":")" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + R"(",
"prefix_hash":")" + epee::to_hex::string(epee::as_byte_span(tx_prefix)) + R"(",
"timestamp":7000,
"fee":100,
"unlock_time":4670,
"height":)" + new_height_str + R"(,
"payment_id":")" + epee::to_hex::string(epee::as_byte_span(payment_id_.long_)) + R"(",
"mixin":16,
"receives":[
{
"amount":40000,
"public_key":")" + epee::to_hex::string(epee::as_byte_span(pub)) + R"(",
"index":2,
"id": {"legacy": {"amount": 0, "index": 100}},
"tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx_public)) + R"(",
"rct":")" + epee::to_hex::string(epee::as_byte_span(ringct)) + R"(",
"recipient":{"maj_i":2,"min_i":66}
},
{
"amount":30000,
"public_key":")" + epee::to_hex::string(epee::as_byte_span(pub2)) + R"(",
"index":1,
"id": {"legacy": {"amount": 0, "index": 99}},
"tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx_public)) + R"(",
"rct":")" + epee::to_hex::string(epee::as_byte_span(ringct)) + R"("
}
],
"spends":[
{
"id": {"legacy": {"amount": 0, "index": 90}},
"key_image":")" + epee::to_hex::string(epee::as_byte_span(image)) + R"("
}
]
}]
})";
EXPECT(!io.stopped());
EXPECT(error == boost::system::error_code{});
EXPECT(get_prefix(response) == "blocks:");
response.erase(0, std::strlen("blocks:"));
verify_json(lest_env, "blocks", response, expected);
pull::async_close(conn, handler);
ran = false;
error = {};
response = {};
while (!ran && !io.stopped())
{
io.restart();
io.run_one();
}
EXPECT(!io.stopped());
EXPECT(error == boost::system::error_code{});
EXPECT(response == "");
// invalid view key
message = R"(login:{"account":{"address":")" + address + R"(","view_key":")" + epee::to_hex::string(epee::as_byte_span(crypto::ec_scalar{})) + R"("}})";
conn = pull::make(io, boost::asio::ip::tcp::endpoint(local, 10000), "lws.feed.v0.json");
pull::async_handshake(conn, message, handler);
ran = false;
error = {};
response = {};
while (!ran && !io.stopped())
{
io.restart();
io.run_one();
}
expected = R"({"msg":"Address/viewkey mismatch", "code":3})";
EXPECT(!io.stopped());
EXPECT(error == boost::system::error_code{});
EXPECT(get_prefix(response) == "error:");
response.erase(0, std::strlen("error:"));
verify_json(lest_env, "view key error", response, expected);
pull::async_next_message(conn, handler);
ran = false;
error = {};
response = {};
while (!ran && !io.stopped())
{
io.restart();
io.run_one();
}
EXPECT(!io.stopped());
EXPECT(error == boost::beast::websocket::error::closed);
}
}
}
+1 -1
View File
@@ -27,7 +27,7 @@
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
add_library(wownero-lws-unit-rpc OBJECT admin.test.cpp)
add_library(wownero-lws-unit-rpc OBJECT admin.test.cpp pull.test.cpp pull.test.h)
target_link_libraries(
wownero-lws-unit-rpc
wownero-lws-unit-db
+140
View File
@@ -0,0 +1,140 @@
// Copyright (c) 2026 The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "pull.test.h"
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/strand.hpp>
#include <boost/beast/core/error.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/core/string_type.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/write.hpp>
#include <boost/beast/version.hpp>
#include <boost/beast/websocket/stream.hpp>
#include "error.h"
#include "misc_log_ex.h"
namespace lws_test { namespace rpc { namespace pull
{
struct connection
{
boost::asio::io_context::strand strand_;
boost::beast::flat_buffer buffer_;
boost::beast::websocket::stream<boost::asio::ip::tcp::socket> stream_;
const boost::asio::ip::tcp::endpoint remote_;
std::string out_;
explicit connection(boost::asio::io_context& io, const boost::asio::ip::tcp::endpoint& remote)
: strand_(io), buffer_(), stream_(io), remote_(remote), out_()
{}
};
namespace
{
struct wrapper
{
std::shared_ptr<connection> self_;
std::function<response> f_;
void operator()(const boost::system::error_code error, std::size_t = {}) const
{
LWS_VERIFY(self_ && f_);
if (error)
f_(error, {});
const boost::beast::net::const_buffer buf = self_->buffer_.cdata();
std::string msg{reinterpret_cast<const char*>(buf.data()), buf.size()};
self_->buffer_.consume(buf.size());
f_(error, std::move(msg));
}
};
class connector : public boost::asio::coroutine
{
wrapper self_;
public:
explicit connector(wrapper&& self)
: boost::asio::coroutine(), self_(std::move(self))
{}
connector(const connector&) = default;
void operator()(const boost::system::error_code error = {}, std::size_t = {})
{
LWS_VERIFY(self_.self_);
connection& self = *self_.self_;
if (error)
return self_(error);
BOOST_ASIO_CORO_REENTER(*this)
{
BOOST_ASIO_CORO_YIELD self.stream_.next_layer().async_connect(self.remote_, *this);
BOOST_ASIO_CORO_YIELD self.stream_.async_handshake("localhost", "/feed", *this);
BOOST_ASIO_CORO_YIELD self.stream_.async_write(boost::asio::const_buffer(self.out_.data(), self.out_.size()), *this);
BOOST_ASIO_CORO_YIELD self.stream_.async_read(self.buffer_, *this);
self_(error);
}
}
};
} // anonymous
std::shared_ptr<connection> make(boost::asio::io_context& io, const boost::asio::ip::tcp::endpoint& remote, const std::string& version)
{
namespace ws = boost::beast::websocket;
namespace http = boost::beast::http;
const auto ptr = std::make_shared<connection>(io, remote);
ptr->stream_.set_option(
ws::stream_base::decorator(
[version] (http::request_header<>& hdr) { hdr.set(http::field::sec_websocket_protocol, version); }
)
);
return ptr;
}
void async_handshake(std::shared_ptr<connection> self, std::string init_message, std::function<response> resp)
{
LWS_VERIFY(self);
self->out_ = std::move(init_message);
connector{wrapper{self, std::move(resp)}}();
}
void async_next_message(std::shared_ptr<connection> self, std::function<response> resp)
{
LWS_VERIFY(self);
self->stream_.async_read(self->buffer_, wrapper{self, std::move(resp)});
}
void async_close(std::shared_ptr<connection> self, std::function<response> resp)
{
LWS_VERIFY(self);
self->stream_.async_close(boost::beast::websocket::close_reason(), wrapper{self, std::move(resp)});
}
}}} // lws_Test // rpc // pull
+49
View File
@@ -0,0 +1,49 @@
// Copyright (c) 2026 The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/system/error_code.hpp>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
namespace lws_test { namespace rpc { namespace pull
{
using response = void(boost::system::error_code, std::string);
struct connection;
std::shared_ptr<connection> make(boost::asio::io_context& io, const boost::asio::ip::tcp::endpoint& remote, const std::string& version);
void async_handshake(std::shared_ptr<connection> self, std::string init_message, std::function<response> resp);
void async_next_message(std::shared_ptr<connection> self, std::function<response> resp);
void async_close(std::shared_ptr<connection> self, std::function<response> resp);
}}} // lws_test // rpc // pull
+15 -154
View File
@@ -38,11 +38,11 @@
#include "db/print.test.h"
#include "db/storage.test.h"
#include "device/device_default.hpp" // monero/src
#include "hardforks/hardforks.h" // monero/src
#include "net/zmq.h" // monero/src
#include "rpc/client.h"
#include "rpc/daemon_messages.h" // monero/src
#include "scanner.h"
#include "util/transaction.test.h"
#include "wire/error.h"
#include "wire/json/write.h"
@@ -87,147 +87,7 @@ namespace
boost::thread& thread;
~join() { thread.join(); }
};
struct transaction
{
cryptonote::transaction tx;
std::vector<crypto::secret_key> additional_keys;
std::vector<crypto::public_key> pub_keys;
std::vector<crypto::public_key> spend_publics;
};
transaction make_miner_tx(lest::env& lest_env, lws::db::block_id height, const lws::db::account_address& miner_address, bool use_view_tags)
{
static constexpr std::uint64_t fee = 0;
transaction tx{};
tx.pub_keys.emplace_back();
tx.spend_publics.emplace_back();
crypto::secret_key key;
crypto::generate_keys(tx.pub_keys.back(), key);
EXPECT(add_tx_pub_key_to_extra(tx.tx, tx.pub_keys.back()));
cryptonote::txin_gen in;
in.height = std::uint64_t(height);
tx.tx.vin.push_back(in);
// This will work, until size of constructed block is less then CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE
uint64_t block_reward;
EXPECT(cryptonote::get_block_reward(0, 0, 1000000, block_reward, num_testnet_hard_forks));
block_reward += fee;
crypto::key_derivation derivation;
EXPECT(crypto::generate_key_derivation(miner_address.view_public, key, derivation));
EXPECT(crypto::derive_public_key(derivation, 0, miner_address.spend_public, tx.spend_publics.back()));
crypto::view_tag view_tag;
if (use_view_tags)
crypto::derive_view_tag(derivation, 0, view_tag);
cryptonote::tx_out out;
cryptonote::set_tx_out(block_reward, tx.spend_publics.back(), use_view_tags, view_tag, out);
tx.tx.vout.push_back(out);
tx.tx.version = 2;
tx.tx.unlock_time = std::uint64_t(height) + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW;
return tx;
}
struct get_spend_public
{
std::vector<crypto::public_key>& pub_keys;
template<typename T>
void operator()(const T&) const noexcept
{}
void operator()(const cryptonote::txout_to_key& val) const
{ pub_keys.push_back(val.key); }
void operator()(const cryptonote::txout_to_tagged_key& val) const
{ pub_keys.push_back(val.key); }
};
transaction make_tx(lest::env& lest_env, const cryptonote::account_keys& keys, std::vector<cryptonote::tx_destination_entry> destinations, const std::uint32_t ring_base, const bool use_view_tag)
{
static constexpr std::uint64_t input_amount = 20000;
static constexpr std::uint64_t output_amount = 8000;
EXPECT(15 < std::numeric_limits<std::uint32_t>::max() - ring_base);
crypto::secret_key unused_key{};
crypto::secret_key og_tx_key{};
crypto::public_key og_tx_public{};
crypto::generate_keys(og_tx_public, og_tx_key);
crypto::key_derivation derivation{};
crypto::public_key spend_public{};
EXPECT(crypto::generate_key_derivation(keys.m_account_address.m_view_public_key, og_tx_key, derivation));
EXPECT(crypto::derive_public_key(derivation, 0, keys.m_account_address.m_spend_public_key, spend_public));
std::uint32_t index = -1;
std::unordered_map<crypto::public_key, cryptonote::subaddress_index> subaddresses;
for (const auto& destination : destinations)
{
++index;
subaddresses[destination.addr.m_spend_public_key] = {0, index};
}
if (2 < destinations.size())
destinations.erase(destinations.begin() + 1, destinations.end() - 1);
std::vector<cryptonote::tx_source_entry> sources;
sources.emplace_back();
sources.back().amount = input_amount;
sources.back().rct = true;
sources.back().real_output = 15;
sources.back().real_output_in_tx_index = 0;
sources.back().real_out_tx_key = og_tx_public;
for (std::uint32_t i = ring_base; i < 15 + ring_base; ++i)
{
crypto::public_key next{};
crypto::generate_keys(next, unused_key);
sources.back().push_output(i, next, 10000);
}
sources.back().outputs.emplace_back();
sources.back().outputs.back().first = 15 + ring_base;
sources.back().outputs.back().second.dest = rct::pk2rct(spend_public);
transaction out{};
EXPECT(
cryptonote::construct_tx_and_get_tx_key(
keys, subaddresses, sources, destinations, keys.m_account_address, {}, out.tx, /* 0, */ unused_key,
out.additional_keys, true, {rct::RangeProofType::RangeProofPaddedBulletproof, 2}, use_view_tag
)
);
for (const auto& vout : out.tx.vout)
boost::apply_visitor(get_spend_public{out.spend_publics}, vout.target);
if (out.additional_keys.empty())
{
std::vector<cryptonote::tx_extra_field> extra;
EXPECT(cryptonote::parse_tx_extra(out.tx.extra, extra));
cryptonote::tx_extra_pub_key key;
EXPECT(cryptonote::find_tx_extra_field_by_type(extra, key));
out.pub_keys.emplace_back();
out.pub_keys.back() = key.pub_key;
}
else
{
for (const auto& this_key : out.additional_keys)
{
out.pub_keys.emplace_back();
EXPECT(crypto::secret_key_to_public_key(this_key, out.pub_keys.back()));
}
}
return out;
}
void scanner_thread(lws::scanner& scanner, void* ctx, const std::vector<epee::byte_slice>& reply)
{
struct stop_
@@ -331,8 +191,9 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
SETUP("lws::rpc::context, ZMQ_REP Server, and lws::db::storage")
{
std::shared_ptr<lws::mempool> pool{};
auto rpc =
lws::rpc::context::make(lws_test::rpc_rendevous, {}, {}, {}, std::chrono::minutes{0}, false);
lws::rpc::context::make(lws_test::rpc_rendevous, {}, {}, {}, std::chrono::minutes{0}, false, true);
lws::db::test::cleanup_db on_scope_exit{};
@@ -448,15 +309,15 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
destinations.back().addr = keys.m_account_address;
std::vector<epee::byte_slice> messages{};
transaction tx = make_miner_tx(lest_env, last_block.id, account, false);
lws_test::transaction tx = lws_test::make_miner_tx(lest_env, last_block.id, account, false);
EXPECT(tx.pub_keys.size() == 1);
EXPECT(tx.spend_publics.size() == 1);
transaction tx2 = make_tx(lest_env, keys, destinations, 20, true);
lws_test::transaction tx2 = lws_test::make_tx(lest_env, keys, destinations, 20, true);
EXPECT(tx2.pub_keys.size() == 1);
EXPECT(tx2.spend_publics.size() == 1);
transaction tx3 = make_tx(lest_env, keys, destinations, 86, false);
lws_test::transaction tx3 = lws_test::make_tx(lest_env, keys, destinations, 86, false);
EXPECT(tx3.pub_keys.size() == 1);
EXPECT(tx3.spend_publics.size() == 1);
@@ -465,7 +326,7 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
destinations.back().addr = keys_subaddr1.m_account_address;
destinations.back().is_subaddress = true;
transaction tx4 = make_tx(lest_env, keys, destinations, 50, false);
lws_test::transaction tx4 = lws_test::make_tx(lest_env, keys, destinations, 50, false);
EXPECT(tx4.pub_keys.size() == 1);
EXPECT(tx4.spend_publics.size() == 2);
@@ -474,7 +335,7 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
//destinations.back().addr = keys_subaddr2.m_account_address;
//destinations.back().is_subaddress = true;
//transaction tx5 = make_tx(lest_env, keys, destinations, 100, true);
//transaction tx5 = lws_test::make_tx(lest_env, keys, destinations, 100, true);
//EXPECT(tx5.pub_keys.size() == 3);
//EXPECT(tx5.spend_publics.size() == 3);
@@ -542,7 +403,7 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
lws::scanner scanner{db.clone(), epee::net_utils::ssl_verification_t::none};
boost::thread server_thread(&scanner_thread, std::ref(scanner), rpc.zmq_context(), std::cref(messages));
const join on_scope_exit{server_thread};
scanner.run(std::move(rpc), 1, {}, {}, opts);
scanner.run(std::move(rpc), pool, 1, {}, {}, opts);
}
hashes.push_back(cryptonote::get_block_hash(bmessage.blocks.back().block));
@@ -749,15 +610,15 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
destinations.back().addr = keys.m_account_address;
std::vector<epee::byte_slice> messages{};
transaction tx = make_miner_tx(lest_env, last_block.id, account, false);
lws_test::transaction tx = lws_test::make_miner_tx(lest_env, last_block.id, account, false);
EXPECT(tx.pub_keys.size() == 1);
EXPECT(tx.spend_publics.size() == 1);
transaction tx2 = make_tx(lest_env, keys, destinations, 20, true);
lws_test::transaction tx2 = lws_test::make_tx(lest_env, keys, destinations, 20, true);
EXPECT(tx2.pub_keys.size() == 1);
EXPECT(tx2.spend_publics.size() == 1);
transaction tx3 = make_tx(lest_env, keys, destinations, 86, false);
lws_test::transaction tx3 = lws_test::make_tx(lest_env, keys, destinations, 86, false);
EXPECT(tx3.pub_keys.size() == 1);
EXPECT(tx3.spend_publics.size() == 1);
@@ -766,7 +627,7 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
destinations.back().addr = keys_subaddr1.m_account_address;
destinations.back().is_subaddress = true;
transaction tx4 = make_tx(lest_env, keys, destinations, 50, false);
lws_test::transaction tx4 = lws_test::make_tx(lest_env, keys, destinations, 50, false);
EXPECT(tx4.pub_keys.size() == 1);
EXPECT(tx4.spend_publics.size() == 2);
@@ -775,7 +636,7 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
destinations.back().addr = keys_subaddr2.m_account_address;
destinations.back().is_subaddress = true;
transaction tx5 = make_tx(lest_env, keys, destinations, 146, true);
lws_test::transaction tx5 = lws_test::make_tx(lest_env, keys, destinations, 146, true);
EXPECT(tx5.pub_keys.size() == 1);
EXPECT(tx5.spend_publics.size() == 2);
@@ -868,7 +729,7 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
lws::scanner scanner{db.clone(), epee::net_utils::ssl_verification_t::none};
boost::thread server_thread(&scanner_thread, std::ref(scanner), rpc.zmq_context(), std::cref(messages));
const join on_scope_exit{server_thread};
scanner.run(std::move(rpc), 1, {}, {}, opts);
scanner.run(std::move(rpc), pool, 1, {}, {}, opts);
}
hashes.push_back(cryptonote::get_block_hash(bmessage.blocks.back().block));
+30
View File
@@ -0,0 +1,30 @@
# Copyright (c) 2024, The Monero Project
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification, are
# permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of
# conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list
# of conditions and the following disclaimer in the documentation and/or other
# materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
add_library(wownero-lws-unit-util account.test.cpp account.test.h transaction.test.cpp transaction.test.h)
target_link_libraries(wownero-lws-unit-util wownero-lws-unit-framework wownero-lws-db wownero::libraries)
+42
View File
@@ -0,0 +1,42 @@
// Copyright (c) 2026, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "account.test.h"
#include "crypto/crypto.h"
#include "cryptonote_basic/account.h"
namespace lws_test
{
cryptonote::account_keys make_account()
{
cryptonote::account_keys out{};
crypto::generate_keys(out.m_account_address.m_spend_public_key, out.m_spend_secret_key);
crypto::generate_keys(out.m_account_address.m_view_public_key, out.m_view_secret_key);
return out;
}
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright (c) 2026, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
namespace cryptonote { class account_keys; }
namespace lws_test
{
cryptonote::account_keys make_account();
}
+222
View File
@@ -0,0 +1,222 @@
// Copyright (c) 2026, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "transaction.test.h"
#include "crypto/crypto.h" // monero/src
#include "cryptonote_basic/account.h" // monero/src
#include "cryptonote_core/cryptonote_tx_utils.h" // monero/src
#include "db/data.h"
#include "hardforks/hardforks.h" // monero/src
#include "util/transactions.h"
namespace lws_test
{
transaction make_miner_tx(lest::env& lest_env, lws::db::block_id height, const lws::db::account_address& miner_address, bool use_view_tags)
{
static constexpr std::uint64_t fee = 0;
transaction tx{};
tx.pub_keys.emplace_back();
tx.spend_publics.emplace_back();
crypto::secret_key key;
crypto::generate_keys(tx.pub_keys.back(), key);
EXPECT(add_tx_pub_key_to_extra(tx.tx, tx.pub_keys.back()));
cryptonote::txin_gen in;
in.height = std::uint64_t(height);
tx.tx.vin.push_back(in);
// This will work, until size of constructed block is less then CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE
uint64_t block_reward;
EXPECT(cryptonote::get_block_reward(0, 0, 1000000, block_reward, num_testnet_hard_forks));
block_reward += fee;
crypto::key_derivation derivation;
EXPECT(crypto::generate_key_derivation(miner_address.view_public, key, derivation));
EXPECT(crypto::derive_public_key(derivation, 0, miner_address.spend_public, tx.spend_publics.back()));
crypto::view_tag view_tag;
if (use_view_tags)
crypto::derive_view_tag(derivation, 0, view_tag);
cryptonote::tx_out out;
cryptonote::set_tx_out(block_reward, tx.spend_publics.back(), use_view_tags, view_tag, out);
tx.tx.vout.push_back(out);
tx.tx.version = 2;
tx.tx.unlock_time = std::uint64_t(height) + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW;
return tx;
}
struct get_key_image
{
std::vector<crypto::key_image>& images;
template<typename T>
void operator()(const T&) const noexcept
{}
void operator()(const cryptonote::txin_to_key& val) const
{
images.push_back(val.k_image);
}
};
struct get_spend_public
{
std::vector<crypto::public_key>& pub_keys;
template<typename T>
void operator()(const T&) const noexcept
{}
void operator()(const cryptonote::txout_to_key& val) const
{ pub_keys.push_back(val.key); }
void operator()(const cryptonote::txout_to_tagged_key& val) const
{ pub_keys.push_back(val.key); }
};
transaction make_tx(lest::env& lest_env, const cryptonote::account_keys& keys, std::vector<cryptonote::tx_destination_entry> destinations, const std::uint32_t ring_base, const bool use_view_tag)
{
static constexpr std::uint64_t input_amount = 20000;
static constexpr std::uint64_t output_amount = 8000;
EXPECT(15 < std::numeric_limits<std::uint32_t>::max() - ring_base);
crypto::secret_key unused_key{};
crypto::secret_key og_tx_key{};
crypto::public_key og_tx_public{};
crypto::generate_keys(og_tx_public, og_tx_key);
crypto::key_derivation derivation{};
crypto::public_key spend_public{};
EXPECT(crypto::generate_key_derivation(keys.m_account_address.m_view_public_key, og_tx_key, derivation));
EXPECT(crypto::derive_public_key(derivation, 0, keys.m_account_address.m_spend_public_key, spend_public));
std::uint32_t index = -1;
std::unordered_map<crypto::public_key, cryptonote::subaddress_index> subaddresses;
for (const auto& destination : destinations)
{
++index;
subaddresses[destination.addr.m_spend_public_key] = {0, index};
}
if (2 < destinations.size())
destinations.erase(destinations.begin() + 1, destinations.end() - 1);
std::vector<cryptonote::tx_source_entry> sources;
sources.emplace_back();
sources.back().amount = input_amount;
sources.back().rct = true;
sources.back().real_output = 15;
sources.back().real_output_in_tx_index = 0;
sources.back().real_out_tx_key = og_tx_public;
for (std::uint32_t i = ring_base; i < 15 + ring_base; ++i)
{
crypto::public_key next{};
crypto::generate_keys(next, unused_key);
sources.back().push_output(i, next, 10000);
}
sources.back().outputs.emplace_back();
sources.back().outputs.back().first = 15 + ring_base;
sources.back().outputs.back().second.dest = rct::pk2rct(spend_public);
transaction out{};
EXPECT(
cryptonote::construct_tx_and_get_tx_key(
keys, subaddresses, sources, destinations, keys.m_account_address, {}, out.tx, /* 0, */ unused_key,
out.additional_keys, true, {rct::RangeProofType::RangeProofPaddedBulletproof, 2}, use_view_tag
)
);
for (const auto& vin : out.tx.vin)
boost::apply_visitor(get_key_image{out.images}, vin);
for (const auto& vout : out.tx.vout)
boost::apply_visitor(get_spend_public{out.spend_publics}, vout.target);
if (out.additional_keys.empty())
{
std::vector<cryptonote::tx_extra_field> extra;
EXPECT(cryptonote::parse_tx_extra(out.tx.extra, extra));
cryptonote::tx_extra_pub_key key;
EXPECT(cryptonote::find_tx_extra_field_by_type(extra, key));
out.pub_keys.emplace_back();
out.pub_keys.back() = key.pub_key;
crypto::key_derivation derivation{};
EXPECT(crypto::generate_key_derivation(key.pub_key, keys.m_view_secret_key, derivation));
for (index = 0; index < out.tx.vout.size(); ++index)
{
const auto decrypted = lws::decode_amount(
out.tx.rct_signatures.outPk.at(index).mask,
out.tx.rct_signatures.ecdhInfo.at(index),
derivation,
index,
true
);
EXPECT(bool(decrypted));
out.ringct.push_back(decrypted->second);
}
}
else
{
index = -1;
for (const auto& this_key : out.additional_keys)
{
++index;
out.pub_keys.emplace_back();
EXPECT(crypto::secret_key_to_public_key(this_key, out.pub_keys.back()));
crypto::key_derivation derivation{};
EXPECT(crypto::generate_key_derivation(out.pub_keys.back(), keys.m_view_secret_key, derivation));
const auto decrypted = lws::decode_amount(
out.tx.rct_signatures.outPk.at(index).mask,
out.tx.rct_signatures.ecdhInfo.at(index),
derivation,
index,
true
);
EXPECT(bool(decrypted));
out.ringct.push_back(decrypted->second);
}
}
for (const auto& rct : out.tx.rct_signatures.ecdhInfo)
out.ringct.push_back(rct.mask);
return out;
}
} // lws_test
+60
View File
@@ -0,0 +1,60 @@
// Copyright (c) 2026, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <cstdint>
#include <vector>
#include "cryptonote_basic/cryptonote_basic.h" // monero/src
#include "db/fwd.h"
#include "lest.hpp"
namespace cryptonote
{
class account_keys;
struct public_key;
class secret_key;
class tx_destination_entry;
}
namespace rct { struct key; }
namespace lws_test
{
struct transaction
{
cryptonote::transaction tx;
std::vector<crypto::secret_key> additional_keys;
std::vector<crypto::public_key> pub_keys;
std::vector<crypto::public_key> spend_publics;
std::vector<crypto::key_image> images;
std::vector<rct::key> ringct;
};
transaction make_miner_tx(lest::env& lest_env, lws::db::block_id height, const lws::db::account_address& miner_address, bool use_view_tags);
transaction make_tx(lest::env& lest_env, const cryptonote::account_keys& keys, std::vector<cryptonote::tx_destination_entry> destinations, const std::uint32_t ring_base, const bool use_view_tag);
}