Compare commits

..

29 Commits

Author SHA1 Message Date
Mayukha Vadari
00d280c965 Merge branch 'develop' into copilot/remove-non-canonical-fields 2026-03-12 14:37:06 -04:00
Vito Tumas
2b14ee3018 refactor: Split combined transactor files into individual classes (#6495)
DID, Escrows, PaymentChannels, and Credentials previously contained multiple unrelated transactor classes in a single header/implementation pair. This change splits each into one class per file, following the same pattern established by the rest of the codebase.
2026-03-12 17:19:29 +00:00
Jingchen
ce31a7ed16 chore: Replace levelization shell script by python script (#6325)
The new python version is significantly faster.
2026-03-12 15:38:00 +00:00
tsinglua
91a23cf80b chore: Fix minor issues in the comments (#6535) 2026-03-12 11:15:30 -04:00
Ayaz Salikhov
e460ea0840 ci: Move Type of Change from PR template to CONTRIBUTING (#6522)
Now that prefixes in PR titles are being validated as part of CI, the "Type of Change" section in the PR template is no longer needed. The prefixes and descriptions in the `CONTRIBUTING.md` file have been updated to reflect the currently supported list.
2026-03-12 06:39:40 +01:00
yinyiqian1
46d5c67a8d fix: Mark SAV and Lending transactions as NotDelegable (#6489)
New transactions should be marked as `NotDelegable`, until the interactions with other transactions have been fully tested and validated.
2026-03-11 21:27:35 +00:00
Mayukha Vadari
ce9ccf844a fix: Remove unneeded import, fix log (#6532)
This change:
* Removes an unneeded import in `DeleteAccount.cpp`.
* Fixes a typo in a log statement in `SetAccount.cpp`.
2026-03-11 19:36:03 +00:00
Sergey Kuznetsov
c791cae1ec test: Fix flaky subscribe tests (#6510)
Subscribe tests have a problem that there is no way to synchronize application running in background threads and test threads. Threads are communicating via websocket messages. When the code is compiled in debug mode with code coverage enabled it executes quite slow, so receiving websocket messages by the client in subscribe tests may time out.

This change does 2 things to fix the problem:
* Increases timeout for receiving a websocket message.
* Decreases the number of tests running in parallel.

While testing the fix for subscribe test another flaky test in ledger replay was found, which has also been addressed.
2026-03-11 18:06:12 +00:00
Alex Kremer
7b3724b7a3 fix: Add missed clang-tidy bugprone-inc-dec-conditions check (#6526) 2026-03-11 14:04:26 +00:00
Ayaz Salikhov
bee2d112c6 ci: Fix how clang-tidy is run when .clang-tidy is changed (#6521) 2026-03-11 14:18:18 +01:00
Bart
01c977bbfe ci: Fix rules used to determine when to upload Conan recipes (#6524)
The refs as previously used pointed to the source branch, not the target branch. However, determining the target branch is different depending on the GitHub event. The pull request logic was incorrect and needed to be fixed, and the logic inside the workflow could be simplified. Both modifications have been made in this commit.
2026-03-11 13:43:58 +01:00
Bart
3baf5454f2 ci: Only upload artifacts in the XRPLF/rippled repository (#6523)
This change will only attempt to upload artifacts for CI runs performed in the XRPLF/rippled repository.
2026-03-11 11:48:40 +01:00
Michael Legleux
24a5cbaa93 chore: Build voidstar on amd64 only (#6481)
* chore: Build voidstar on amd64 only

* fatal error if configuring voidstar on  non x86
2026-03-10 23:59:43 +00:00
Michael Legleux
eb7c8c6c7a chore: Use CMake components for install (#6485)
* chore: Use components for install

* rm CMake export targets

* reformat
2026-03-10 23:38:43 +00:00
Alex Kremer
f27d8f3890 chore: Enable clang-tidy bugprone-inc-dec-in-conditions check (#6455) 2026-03-10 20:12:15 +00:00
Alex Kremer
8345cd77df chore: Enable clang-tidy bugprone-unused-raii check (#6505) 2026-03-10 19:48:56 +00:00
Alex Kremer
c38aabdaee chore: Enable clang-tidy bugprone-unhandled-self-assignment check (#6504) 2026-03-10 17:42:49 +00:00
Alex Kremer
a896ed3987 chore: Enable clang-tidy bugprone-optional-value-conversion check (#6470) 2026-03-10 15:56:24 +01:00
Alex Kremer
1a7d67c4db chore: Enable clang-tidy bugprone-reserved-identifier check (#6456) 2026-03-10 10:29:08 +01:00
Alex Kremer
92983d8040 chore: Enable clang-tidy bugprone-too-small-loop-variable check (#6473) 2026-03-10 08:56:44 +00:00
Alex Kremer
320a65f77c chore: Enable clang-tidy bugprone-suspicious-stringview-data-usage check (#6467) 2026-03-10 08:34:27 +00:00
Ayaz Salikhov
45b8c4d732 chore: Update XRPLF/actions (#6508)
This change mainly includes XRPLF/actions#51.
2026-03-09 21:47:22 +00:00
Alex Kremer
e284969ae4 chore: Enable clang-tidy bugprone-pointer-arithmetic-on-polymorphic-object check (#6469) 2026-03-09 19:36:56 +01:00
Mayukha Vadari
ad116a35ff Merge branch 'develop' into copilot/remove-non-canonical-fields 2026-03-02 17:06:25 -05:00
Mayukha Vadari
249fb12e8f Merge branch 'develop' into copilot/remove-non-canonical-fields 2026-02-27 16:41:47 -05:00
Mayukha Vadari
cbabee1bec Merge branch 'develop' into copilot/remove-non-canonical-fields 2026-02-27 13:44:49 -05:00
copilot-swe-agent[bot]
cf2835e3c1 Fix date/ctid missing from result level in API v3, fix pre-commit errors
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2026-02-27 17:36:19 +00:00
copilot-swe-agent[bot]
9b0e87a37e Fix: remove non-canonical fields from tx_json in API v3
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2026-02-27 17:02:28 +00:00
copilot-swe-agent[bot]
4a31ee1926 Initial plan 2026-02-27 16:52:00 +00:00
125 changed files with 3391 additions and 14300 deletions

View File

@@ -14,6 +14,7 @@ Checks: "-*,
bugprone-fold-init-type,
bugprone-forward-declaration-namespace,
bugprone-inaccurate-erase,
bugprone-inc-dec-in-conditions,
bugprone-incorrect-enable-if,
bugprone-incorrect-roundings,
bugprone-infinite-loop,
@@ -30,9 +31,12 @@ Checks: "-*,
bugprone-multiple-statement-macro,
bugprone-no-escape,
bugprone-non-zero-enum-to-bool-conversion,
bugprone-optional-value-conversion,
bugprone-parent-virtual-call,
bugprone-pointer-arithmetic-on-polymorphic-object,
bugprone-posix-return,
bugprone-redundant-branch-condition,
bugprone-reserved-identifier,
bugprone-return-const-ref-from-parameter,
bugprone-shared-ptr-array-mismatch,
bugprone-signal-handler,
@@ -53,14 +57,18 @@ Checks: "-*,
bugprone-suspicious-realloc-usage,
bugprone-suspicious-semicolon,
bugprone-suspicious-string-compare,
bugprone-suspicious-stringview-data-usage,
bugprone-swapped-arguments,
bugprone-terminating-continue,
bugprone-throw-keyword-missing,
bugprone-too-small-loop-variable,
bugprone-undefined-memory-manipulation,
bugprone-undelegated-constructor,
bugprone-unhandled-exception-at-new,
bugprone-unhandled-self-assignment,
bugprone-unique-ptr-array-mismatch,
bugprone-unsafe-functions,
bugprone-unused-raii,
bugprone-unused-local-non-trivial-variable,
bugprone-virtual-near-miss,
cppcoreguidelines-no-suspend-with-lock,
@@ -90,18 +98,11 @@ Checks: "-*,
# checks that have some issues that need to be resolved:
#
# bugprone-crtp-constructor-accessibility,
# bugprone-inc-dec-in-conditions,
# bugprone-reserved-identifier,
# bugprone-move-forwarding-reference,
# bugprone-switch-missing-default-case,
# bugprone-suspicious-stringview-data-usage,
# bugprone-pointer-arithmetic-on-polymorphic-object,
# bugprone-optional-value-conversion,
# bugprone-too-small-loop-variable,
# bugprone-unused-raii,
# bugprone-unused-return-value,
# bugprone-use-after-move,
# bugprone-unhandled-self-assignment,
# bugprone-unused-raii,
#
# cppcoreguidelines-misleading-capture-default-by-value,
# cppcoreguidelines-init-variables,

View File

@@ -29,22 +29,6 @@ If a refactor, how is this better than the previous implementation?
If there is a spec or design document for this feature, please link it here.
-->
### Type of Change
<!--
Please check [x] relevant options, delete irrelevant ones.
-->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Refactor (non-breaking change that only restructures code)
- [ ] Performance (increase or change in throughput and/or latency)
- [ ] Tests (you added tests for code that already exists, or your new feature included in this PR)
- [ ] Documentation update
- [ ] Chore (no impact to binary, e.g. `.gitignore`, formatting, dropping support for older tooling)
- [ ] Release
### API Impact
<!--

View File

@@ -70,7 +70,7 @@ that `test` code should _never_ be included in `xrpl` or `xrpld` code.)
## Validation
The [levelization](generate.sh) script takes no parameters,
The [levelization](generate.py) script takes no parameters,
reads no environment variables, and can be run from any directory,
as long as it is in the expected location in the rippled repo.
It can be run at any time from within a checked out repo, and will
@@ -104,7 +104,7 @@ It generates many files of [results](results):
Github Actions workflow to test that levelization loops haven't
changed. Unfortunately, if changes are detected, it can't tell if
they are improvements or not, so if you have resolved any issues or
done anything else to improve levelization, run `levelization.sh`,
done anything else to improve levelization, run `generate.py`,
and commit the updated results.
The `loops.txt` and `ordering.txt` files relate the modules
@@ -128,7 +128,7 @@ The committed files hide the detailed values intentionally, to
prevent false alarms and merging issues, and because it's easy to
get those details locally.
1. Run `levelization.sh`
1. Run `generate.py`
2. Grep the modules in `paths.txt`.
- For example, if a cycle is found `A ~= B`, simply `grep -w
A .github/scripts/levelization/results/paths.txt | grep -w B`

335
.github/scripts/levelization/generate.py vendored Normal file
View File

@@ -0,0 +1,335 @@
#!/usr/bin/env python3
"""
Usage: generate.py
This script takes no parameters, and can be called from any directory in the file system.
"""
import os
import re
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
from typing import Dict, List, Tuple, Set, Optional
# Compile regex patterns once at module level
INCLUDE_PATTERN = re.compile(r"^\s*#include.*/.*\.h")
INCLUDE_PATH_PATTERN = re.compile(r'[<"]([^>"]+)[>"]')
def dictionary_sort_key(s: str) -> str:
"""
Create a sort key that mimics 'sort -d' (dictionary order).
Dictionary order only considers blanks and alphanumeric characters.
This means punctuation like '.' is ignored during sorting.
"""
# Keep only alphanumeric characters and spaces
return "".join(c for c in s if c.isalnum() or c.isspace())
def get_level(file_path: str) -> str:
"""
Extract the level from a file path (second and third directory components).
Equivalent to bash: cut -d/ -f 2,3
Examples:
src/xrpld/app/main.cpp -> xrpld.app
src/libxrpl/protocol/STObject.cpp -> libxrpl.protocol
include/xrpl/basics/base_uint.h -> xrpl.basics
"""
parts = file_path.split("/")
# Get fields 2 and 3 (indices 1 and 2 in 0-based indexing)
if len(parts) >= 3:
level = f"{parts[1]}/{parts[2]}"
elif len(parts) >= 2:
level = f"{parts[1]}/toplevel"
else:
level = file_path
# If the "level" indicates a file, cut off the filename
if "." in level.split("/")[-1]: # Avoid Path object creation
# Use the "toplevel" label as a workaround for `sort`
# inconsistencies between different utility versions
level = level.rsplit("/", 1)[0] + "/toplevel"
return level.replace("/", ".")
def extract_include_level(include_line: str) -> Optional[str]:
"""
Extract the include path from an #include directive.
Gets the first two directory components from the include path.
Equivalent to bash: cut -d/ -f 1,2
Examples:
#include <xrpl/basics/base_uint.h> -> xrpl.basics
#include "xrpld/app/main/Application.h" -> xrpld.app
"""
# Remove everything before the quote or angle bracket
match = INCLUDE_PATH_PATTERN.search(include_line)
if not match:
return None
include_path = match.group(1)
parts = include_path.split("/")
# Get first two fields (indices 0 and 1)
if len(parts) >= 2:
include_level = f"{parts[0]}/{parts[1]}"
else:
include_level = include_path
# If the "includelevel" indicates a file, cut off the filename
if "." in include_level.split("/")[-1]: # Avoid Path object creation
include_level = include_level.rsplit("/", 1)[0] + "/toplevel"
return include_level.replace("/", ".")
def find_repository_directories(
start_path: Path, depth_limit: int = 10
) -> Tuple[Path, List[Path]]:
"""
Find the repository root by looking for src or include folders.
Walks up the directory tree from the start path.
"""
current = start_path.resolve()
# Walk up the directory tree
for _ in range(depth_limit): # Limit search depth to prevent infinite loops
src_path = current / "src"
include_path = current / "include"
# Check if this directory has src or include folders
has_src = src_path.exists()
has_include = include_path.exists()
if has_src or has_include:
return current, [src_path, include_path]
# Move up one level
parent = current.parent
if parent == current: # Reached filesystem root
break
current = parent
# If we couldn't find it, raise an error
raise RuntimeError(
"Could not find repository root. "
"Expected to find a directory containing 'src' and/or 'include' folders."
)
def main():
# Change to the script's directory
script_dir = Path(__file__).parent.resolve()
os.chdir(script_dir)
# Clean up and create results directory.
results_dir = script_dir / "results"
if results_dir.exists():
import shutil
shutil.rmtree(results_dir)
results_dir.mkdir()
# Find the repository root by searching for src and include directories.
try:
repo_root, scan_dirs = find_repository_directories(script_dir)
print(f"Found repository root: {repo_root}")
print(f"Scanning directories:")
for scan_dir in scan_dirs:
print(f" - {scan_dir.relative_to(repo_root)}")
except RuntimeError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
print("\nScanning for raw includes...")
# Find all #include directives
raw_includes: List[Tuple[str, str]] = []
rawincludes_file = results_dir / "rawincludes.txt"
# Write to file as we go to avoid storing everything in memory.
with open(rawincludes_file, "w", buffering=8192) as raw_f:
for dir_path in scan_dirs:
print(f" Scanning {dir_path.relative_to(repo_root)}...")
for file_path in dir_path.rglob("*"):
if not file_path.is_file():
continue
try:
rel_path_str = str(file_path.relative_to(repo_root))
# Read file with a large buffer for performance.
with open(
file_path,
"r",
encoding="utf-8",
errors="ignore",
buffering=8192,
) as f:
for line in f:
# Quick check before regex
if "#include" not in line or "boost" in line:
continue
if INCLUDE_PATTERN.match(line):
line_stripped = line.strip()
entry = f"{rel_path_str}:{line_stripped}\n"
print(entry, end="")
raw_f.write(entry)
raw_includes.append((rel_path_str, line_stripped))
except Exception as e:
print(f"Error reading {file_path}: {e}", file=sys.stderr)
# Build levelization paths and count directly (no need to sort first).
print("Build levelization paths")
path_counts: Dict[Tuple[str, str], int] = defaultdict(int)
for file_path, include_line in raw_includes:
include_level = extract_include_level(include_line)
if not include_level:
continue
level = get_level(file_path)
if level != include_level:
path_counts[(level, include_level)] += 1
# Sort and deduplicate paths (using dictionary order like bash 'sort -d').
print("Sort and deduplicate paths")
paths_file = results_dir / "paths.txt"
with open(paths_file, "w") as f:
# Sort using dictionary order: only alphanumeric and spaces matter
sorted_items = sorted(
path_counts.items(),
key=lambda x: (dictionary_sort_key(x[0][0]), dictionary_sort_key(x[0][1])),
)
for (level, include_level), count in sorted_items:
line = f"{count:7} {level} {include_level}\n"
print(line.rstrip())
f.write(line)
# Split into flat-file database
print("Split into flat-file database")
includes_dir = results_dir / "includes"
included_by_dir = results_dir / "included_by"
includes_dir.mkdir()
included_by_dir.mkdir()
# Batch writes by grouping data first to avoid repeated file opens.
includes_data: Dict[str, List[Tuple[str, int]]] = defaultdict(list)
included_by_data: Dict[str, List[Tuple[str, int]]] = defaultdict(list)
# Process in sorted order to match bash script behaviour (dictionary order).
sorted_items = sorted(
path_counts.items(),
key=lambda x: (dictionary_sort_key(x[0][0]), dictionary_sort_key(x[0][1])),
)
for (level, include_level), count in sorted_items:
includes_data[level].append((include_level, count))
included_by_data[include_level].append((level, count))
# Write all includes files in sorted order (dictionary order).
for level in sorted(includes_data.keys(), key=dictionary_sort_key):
entries = includes_data[level]
with open(includes_dir / level, "w") as f:
for include_level, count in entries:
line = f"{include_level} {count}\n"
print(line.rstrip())
f.write(line)
# Write all included_by files in sorted order (dictionary order).
for include_level in sorted(included_by_data.keys(), key=dictionary_sort_key):
entries = included_by_data[include_level]
with open(included_by_dir / include_level, "w") as f:
for level, count in entries:
line = f"{level} {count}\n"
print(line.rstrip())
f.write(line)
# Search for loops
print("Search for loops")
loops_file = results_dir / "loops.txt"
ordering_file = results_dir / "ordering.txt"
loops_found: Set[Tuple[str, str]] = set()
# Pre-load all include files into memory to avoid repeated I/O.
# This is the biggest optimisation - we were reading files repeatedly in nested loops.
# Use list of tuples to preserve file order.
includes_cache: Dict[str, List[Tuple[str, int]]] = {}
includes_lookup: Dict[str, Dict[str, int]] = {} # For fast lookup
# Note: bash script uses 'for source in *' which uses standard glob sorting,
# NOT dictionary order. So we use standard sorted() here, not dictionary_sort_key.
for include_file in sorted(includes_dir.iterdir(), key=lambda p: p.name):
if not include_file.is_file():
continue
includes_cache[include_file.name] = []
includes_lookup[include_file.name] = {}
with open(include_file, "r") as f:
for line in f:
parts = line.strip().split()
if len(parts) >= 2:
include_name = parts[0]
include_count = int(parts[1])
includes_cache[include_file.name].append(
(include_name, include_count)
)
includes_lookup[include_file.name][include_name] = include_count
with open(loops_file, "w", buffering=8192) as loops_f, open(
ordering_file, "w", buffering=8192
) as ordering_f:
# Use standard sorting to match bash glob expansion 'for source in *'.
for source in sorted(includes_cache.keys()):
source_includes = includes_cache[source]
for include, include_freq in source_includes:
# Check if include file exists and references source
if include not in includes_lookup:
continue
source_freq = includes_lookup[include].get(source)
if source_freq is not None:
# Found a loop
loop_key = tuple(sorted([source, include]))
if loop_key in loops_found:
continue
loops_found.add(loop_key)
loops_f.write(f"Loop: {source} {include}\n")
# If the counts are close, indicate that the two modules are
# on the same level, though they shouldn't be.
diff = include_freq - source_freq
if diff > 3:
loops_f.write(f" {source} > {include}\n\n")
elif diff < -3:
loops_f.write(f" {include} > {source}\n\n")
elif source_freq == include_freq:
loops_f.write(f" {include} == {source}\n\n")
else:
loops_f.write(f" {include} ~= {source}\n\n")
else:
ordering_f.write(f"{source} > {include}\n")
# Print results
print("\nOrdering:")
with open(ordering_file, "r") as f:
print(f.read(), end="")
print("\nLoops:")
with open(loops_file, "r") as f:
print(f.read(), end="")
if __name__ == "__main__":
main()

View File

@@ -1,130 +0,0 @@
#!/bin/bash
# Usage: generate.sh
# This script takes no parameters, reads no environment variables,
# and can be run from any directory, as long as it is in the expected
# location in the repo.
pushd $( dirname $0 )
if [ -v PS1 ]
then
# if the shell is interactive, clean up any flotsam before analyzing
git clean -ix
fi
# Ensure all sorting is ASCII-order consistently across platforms.
export LANG=C
rm -rfv results
mkdir results
includes="$( pwd )/results/rawincludes.txt"
pushd ../../..
echo Raw includes:
grep -r '^[ ]*#include.*/.*\.h' include src | \
grep -v boost | tee ${includes}
popd
pushd results
oldifs=${IFS}
IFS=:
mkdir includes
mkdir included_by
echo Build levelization paths
exec 3< ${includes} # open rawincludes.txt for input
while read -r -u 3 file include
do
level=$( echo ${file} | cut -d/ -f 2,3 )
# If the "level" indicates a file, cut off the filename
if [[ "${level##*.}" != "${level}" ]]
then
# Use the "toplevel" label as a workaround for `sort`
# inconsistencies between different utility versions
level="$( dirname ${level} )/toplevel"
fi
level=$( echo ${level} | tr '/' '.' )
includelevel=$( echo ${include} | sed 's/.*["<]//; s/[">].*//' | \
cut -d/ -f 1,2 )
if [[ "${includelevel##*.}" != "${includelevel}" ]]
then
# Use the "toplevel" label as a workaround for `sort`
# inconsistencies between different utility versions
includelevel="$( dirname ${includelevel} )/toplevel"
fi
includelevel=$( echo ${includelevel} | tr '/' '.' )
if [[ "$level" != "$includelevel" ]]
then
echo $level $includelevel | tee -a paths.txt
fi
done
echo Sort and deduplicate paths
sort -ds paths.txt | uniq -c | tee sortedpaths.txt
mv sortedpaths.txt paths.txt
exec 3>&- #close fd 3
IFS=${oldifs}
unset oldifs
echo Split into flat-file database
exec 4<paths.txt # open paths.txt for input
while read -r -u 4 count level include
do
echo ${include} ${count} | tee -a includes/${level}
echo ${level} ${count} | tee -a included_by/${include}
done
exec 4>&- #close fd 4
loops="$( pwd )/loops.txt"
ordering="$( pwd )/ordering.txt"
pushd includes
echo Search for loops
# Redirect stdout to a file
exec 4>&1
exec 1>"${loops}"
for source in *
do
if [[ -f "$source" ]]
then
exec 5<"${source}" # open for input
while read -r -u 5 include includefreq
do
if [[ -f $include ]]
then
if grep -q -w $source $include
then
if grep -q -w "Loop: $include $source" "${loops}"
then
continue
fi
sourcefreq=$( grep -w $source $include | cut -d\ -f2 )
echo "Loop: $source $include"
# If the counts are close, indicate that the two modules are
# on the same level, though they shouldn't be
if [[ $(( $includefreq - $sourcefreq )) -gt 3 ]]
then
echo -e " $source > $include\n"
elif [[ $(( $sourcefreq - $includefreq )) -gt 3 ]]
then
echo -e " $include > $source\n"
elif [[ $sourcefreq -eq $includefreq ]]
then
echo -e " $include == $source\n"
else
echo -e " $include ~= $source\n"
fi
else
echo "$source > $include" >> "${ordering}"
fi
fi
done
exec 5>&- #close fd 5
fi
done
exec 1>&4 #close fd 1
exec 4>&- #close fd 4
cat "${ordering}"
cat "${loops}"
popd
popd
popd

View File

@@ -55,7 +55,7 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
# fee to 500.
# - Bookworm using GCC 15: Debug on linux/amd64, enable code
# coverage (which will be done below).
# - Bookworm using Clang 16: Debug on linux/arm64, enable voidstar.
# - Bookworm using Clang 16: Debug on linux/amd64, enable voidstar.
# - Bookworm using Clang 17: Release on linux/amd64, set the
# reference fee to 1000.
# - Bookworm using Clang 20: Debug on linux/amd64.
@@ -78,7 +78,7 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
if (
f"{os['compiler_name']}-{os['compiler_version']}" == "clang-16"
and build_type == "Debug"
and architecture["platform"] == "linux/arm64"
and architecture["platform"] == "linux/amd64"
):
cmake_args = f"-Dvoidstar=ON {cmake_args}"
skip = False

View File

@@ -141,9 +141,8 @@ jobs:
needs:
- should-run
- build-test
# Only run when committing to a PR that targets a release branch in the
# XRPLF repository.
if: ${{ github.repository_owner == 'XRPLF' && needs.should-run.outputs.go == 'true' && startsWith(github.ref, 'refs/heads/release') }}
# Only run when committing to a PR that targets a release branch.
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
uses: ./.github/workflows/reusable-upload-recipe.yml
secrets:
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}

View File

@@ -17,8 +17,7 @@ defaults:
jobs:
upload-recipe:
# Only run when a tag is pushed to the XRPLF repository.
if: ${{ github.repository_owner == 'XRPLF' }}
if: ${{ github.repository == 'XRPLF/rippled' }}
uses: ./.github/workflows/reusable-upload-recipe.yml
secrets:
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}

View File

@@ -92,8 +92,8 @@ jobs:
upload-recipe:
needs: build-test
# Only run when pushing to the develop branch in the XRPLF repository.
if: ${{ github.repository_owner == 'XRPLF' && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
# Only run when pushing to the develop branch.
if: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
uses: ./.github/workflows/reusable-upload-recipe.yml
secrets:
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}

View File

@@ -11,7 +11,7 @@ on:
jobs:
# Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks.
run-hooks:
uses: XRPLF/actions/.github/workflows/pre-commit.yml@56de1bdf19639e009639a50b8d17c28ca954f267
uses: XRPLF/actions/.github/workflows/pre-commit.yml@44856eb0d6ecb7d376370244324ab3dc8b863bad
with:
runs_on: ubuntu-latest
container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }'

View File

@@ -176,7 +176,7 @@ jobs:
fi
- name: Upload the binary (Linux)
if: ${{ github.repository_owner == 'XRPLF' && runner.os == 'Linux' }}
if: ${{ github.repository == 'XRPLF/rippled' && runner.os == 'Linux' }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: xrpld-${{ inputs.config_name }}
@@ -230,6 +230,8 @@ jobs:
BUILD_NPROC: ${{ steps.nproc.outputs.nproc }}
run: |
set -o pipefail
# Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness
[ "$COVERAGE_ENABLED" = "true" ] && BUILD_NPROC=$(( BUILD_NPROC - 2 ))
./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log
- name: Show test failure summary
@@ -266,7 +268,7 @@ jobs:
--target coverage
- name: Upload coverage report
if: ${{ github.repository_owner == 'XRPLF' && !inputs.build_only && env.COVERAGE_ENABLED == 'true' }}
if: ${{ github.repository == 'XRPLF/rippled' && !inputs.build_only && env.COVERAGE_ENABLED == 'true' }}
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
disable_search: true

View File

@@ -20,7 +20,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check levelization
run: .github/scripts/levelization/generate.sh
run: python .github/scripts/levelization/generate.py
- name: Check for differences
env:
MESSAGE: |
@@ -32,7 +32,7 @@ jobs:
removed from loops.txt, it's probably an improvement, while if
something was added, it's probably a regression.
Run '.github/scripts/levelization/generate.sh' in your repo, commit
Run '.github/scripts/levelization/generate.py' in your repo, commit
and push the changes. See .github/scripts/levelization/README.md for
more info.
run: |

View File

@@ -51,5 +51,5 @@ jobs:
if: ${{ always() && !cancelled() && (!inputs.check_only_changed || needs.determine-files.outputs.any_cpp_changed == 'true' || needs.determine-files.outputs.clang_tidy_config_changed == 'true') }}
uses: ./.github/workflows/reusable-clang-tidy-files.yml
with:
files: ${{ (needs.determine-files.outputs.clang_tidy_config_changed == 'true' && '') || (inputs.check_only_changed && needs.determine-files.outputs.all_changed_files || '') }}
files: ${{ needs.determine-files.outputs.clang_tidy_config_changed == 'true' && '' || (inputs.check_only_changed && needs.determine-files.outputs.all_changed_files || '') }}
create_issue_on_failure: ${{ inputs.create_issue_on_failure }}

View File

@@ -69,22 +69,28 @@ jobs:
conan export . --version=${{ steps.version.outputs.version }}
conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/${{ steps.version.outputs.version }}
# When this workflow is triggered by a push event, it will always be when merging into the
# 'develop' branch, see on-trigger.yml.
- name: Upload Conan recipe (develop)
if: ${{ github.ref == 'refs/heads/develop' }}
if: ${{ github.event_name == 'push' }}
env:
REMOTE_NAME: ${{ inputs.remote_name }}
run: |
conan export . --version=develop
conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/develop
# When this workflow is triggered by a pull request event, it will always be when merging into
# one of the 'release' branches, see on-pr.yml.
- name: Upload Conan recipe (rc)
if: ${{ startsWith(github.ref, 'refs/heads/release') }}
if: ${{ github.event_name == 'pull_request' }}
env:
REMOTE_NAME: ${{ inputs.remote_name }}
run: |
conan export . --version=rc
conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/rc
# When this workflow is triggered by a tag event, it will always be when tagging a final
# release, see on-tag.yml.
- name: Upload Conan recipe (release)
if: ${{ github.event_name == 'tag' }}
env:

View File

@@ -103,11 +103,11 @@ jobs:
sanitizers: ${{ matrix.sanitizers }}
- name: Log into Conan remote
if: ${{ github.repository_owner == 'XRPLF' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
if: ${{ github.repository == 'XRPLF/rippled' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
run: conan remote login "${CONAN_REMOTE_NAME}" "${{ secrets.CONAN_REMOTE_USERNAME }}" --password "${{ secrets.CONAN_REMOTE_PASSWORD }}"
- name: Upload Conan packages
if: ${{ github.repository_owner == 'XRPLF' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
if: ${{ github.repository == 'XRPLF/rippled' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
env:
FORCE_OPTION: ${{ github.event.inputs.force_upload == 'true' && '--force' || '' }}
run: conan upload "*" --remote="${CONAN_REMOTE_NAME}" --confirm ${FORCE_OPTION}

3
.gitignore vendored
View File

@@ -75,6 +75,9 @@ DerivedData
/.claude
/CLAUDE.md
# Python
__pycache__
# Direnv's directory
/.direnv

View File

@@ -6,6 +6,13 @@ For info about how [API versioning](https://xrpl.org/request-formatting.html#api
## Breaking Changes
### Modifications to `tx` and `account_tx`
In API version 2, the `tx_json` field in `tx` and `account_tx` responses includes server-added lower-case fields (`date`, `ledger_index`, and `ctid`) that are not part of the canonical signed transaction. In API version 3, these fields are removed from `tx_json` and are only present at the top-level result object.
- **Before (API v2)**: The `tx_json` object in the response contained `date`, `ledger_index`, and `ctid` fields alongside the canonical PascalCase transaction fields.
- **After (API v3)**: The `tx_json` object contains only the canonical signed transaction fields. The `date`, `ledger_index`, and `ctid` fields appear exclusively at the top-level result object.
### Modifications to `amm_info`
The order of error checks has been changed to provide more specific error messages. ([#4924](https://github.com/XRPLF/rippled/pull/4924))

View File

@@ -88,7 +88,6 @@ find_package(ed25519 REQUIRED)
find_package(gRPC REQUIRED)
find_package(LibArchive REQUIRED)
find_package(lz4 REQUIRED)
find_package(mpt-crypto REQUIRED)
find_package(nudb REQUIRED)
find_package(OpenSSL REQUIRED)
find_package(secp256k1 REQUIRED)
@@ -101,7 +100,6 @@ target_link_libraries(
INTERFACE
ed25519::ed25519
lz4::lz4
mpt-crypto::mpt-crypto
OpenSSL::Crypto
OpenSSL::SSL
secp256k1::secp256k1
@@ -133,7 +131,6 @@ if(coverage)
include(XrplCov)
endif()
set(PROJECT_EXPORT_SET XrplExports)
include(XrplCore)
include(XrplInstall)
include(XrplValidatorKeys)

View File

@@ -127,26 +127,6 @@ tl;dr
> 6. Wrap the body at 72 characters.
> 7. Use the body to explain what and why vs. how.
In addition to those guidelines, please add one of the following
prefixes to the subject line if appropriate.
- `fix:` - The primary purpose is to fix an existing bug.
- `perf:` - The primary purpose is performance improvements.
- `refactor:` - The changes refactor code without affecting
functionality.
- `test:` - The changes _only_ affect unit tests.
- `docs:` - The changes _only_ affect documentation. This can
include code comments in addition to `.md` files like this one.
- `build:` - The changes _only_ affect the build process,
including CMake and/or Conan settings.
- `chore:` - Other tasks that don't affect the binary, but don't fit
any of the other cases. e.g. formatting, git settings, updating
Github Actions jobs.
Whenever possible, when updating commits after the PR is open, please
add the PR number to the end of the subject line. e.g. `test: Add
unit tests for Feature X (#1234)`.
## Pull requests
In general, pull requests use `develop` as the base branch.
@@ -180,6 +160,23 @@ credibility of the existing approvals is insufficient.
Pull requests must be merged by [squash-and-merge][squash]
to preserve a linear history for the `develop` branch.
### Type of Change
In addition to those guidelines, please start your PR title with one of the following:
- `build:` - The changes _only_ affect the build process, including CMake and/or Conan settings.
- `feat`: New feature (change which adds functionality).
- `fix:` - The primary purpose is to fix an existing bug.
- `docs:` - The changes _only_ affect documentation.
- `test:` - The changes _only_ affect unit tests.
- `ci`: Continuous Integration (changes to our CI configuration files and scripts).
- `style`: Code style (formatting).
- `refactor:` - The changes refactor code without affecting functionality.
- `perf:` - The primary purpose is performance improvements.
- `chore:` - Other tasks that don't affect the binary, but don't fit any of the other cases. e.g. `git` settings, `clang-tidy`, removing dead code, dropping support for older tooling.
First letter after the type prefix should be capitalized, and the type prefix should be followed by a colon and a space. e.g. `feat: Add support for Borrowing Protocol`.
### "Ready to merge"
A pull request should only have the "Ready to merge" label added when it

View File

@@ -1,60 +0,0 @@
include(CMakeFindDependencyMacro)
# need to represent system dependencies of the lib here
#[=========================================================[
Boost
#]=========================================================]
if(static OR APPLE OR MSVC)
set(Boost_USE_STATIC_LIBS ON)
endif()
set(Boost_USE_MULTITHREADED ON)
if(static OR MSVC)
set(Boost_USE_STATIC_RUNTIME ON)
else()
set(Boost_USE_STATIC_RUNTIME OFF)
endif()
find_dependency(
Boost
COMPONENTS
chrono
container
context
coroutine
date_time
filesystem
program_options
regex
system
thread
)
#[=========================================================[
OpenSSL
#]=========================================================]
if(NOT DEFINED OPENSSL_ROOT_DIR)
if(DEFINED ENV{OPENSSL_ROOT})
set(OPENSSL_ROOT_DIR $ENV{OPENSSL_ROOT})
elseif(APPLE)
find_program(homebrew brew)
if(homebrew)
execute_process(
COMMAND ${homebrew} --prefix openssl
OUTPUT_VARIABLE OPENSSL_ROOT_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
)
endif()
endif()
file(TO_CMAKE_PATH "${OPENSSL_ROOT_DIR}" OPENSSL_ROOT_DIR)
endif()
if(static OR APPLE OR MSVC)
set(OPENSSL_USE_STATIC_LIBS ON)
endif()
set(OPENSSL_MSVC_STATIC_RT ON)
find_dependency(OpenSSL REQUIRED)
find_dependency(ZLIB)
find_dependency(date)
if(TARGET ZLIB::ZLIB)
set_target_properties(
OpenSSL::Crypto
PROPERTIES INTERFACE_LINK_LIBRARIES ZLIB::ZLIB
)
endif()

View File

@@ -2,100 +2,38 @@
install stuff
#]===================================================================]
include(create_symbolic_link)
include(GNUInstallDirs)
# If no suffix is defined for executables (e.g. Windows uses .exe but Linux
# and macOS use none), then explicitly set it to the empty string.
if(NOT DEFINED suffix)
set(suffix "")
if(is_root_project AND TARGET xrpld)
install(
TARGETS xrpld
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime
)
install(
FILES "${CMAKE_CURRENT_SOURCE_DIR}/cfg/xrpld-example.cfg"
DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}/xrpld"
RENAME xrpld.cfg
COMPONENT runtime
)
install(
FILES "${CMAKE_CURRENT_SOURCE_DIR}/cfg/validators-example.txt"
DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}/xrpld"
RENAME validators.txt
COMPONENT runtime
)
endif()
install(
TARGETS
common
opts
xrpl_boost
xrpl_libs
xrpl_syslibs
xrpl.imports.main
xrpl.libpb
xrpl.libxrpl
xrpl.libxrpl.basics
xrpl.libxrpl.beast
xrpl.libxrpl.conditions
xrpl.libxrpl.core
xrpl.libxrpl.crypto
xrpl.libxrpl.git
xrpl.libxrpl.json
xrpl.libxrpl.rdb
xrpl.libxrpl.ledger
xrpl.libxrpl.net
xrpl.libxrpl.nodestore
xrpl.libxrpl.protocol
xrpl.libxrpl.resource
xrpl.libxrpl.server
xrpl.libxrpl.shamap
xrpl.libxrpl.tx
antithesis-sdk-cpp
EXPORT XrplExports
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
TARGETS xrpl.libpb xrpl.libxrpl
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT development
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT development
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT development
)
install(
DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/xrpl"
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
)
install(
EXPORT XrplExports
FILE XrplTargets.cmake
NAMESPACE Xrpl::
DESTINATION lib/cmake/xrpl
)
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
XrplConfigVersion.cmake
VERSION ${xrpld_version}
COMPATIBILITY SameMajorVersion
)
if(is_root_project AND TARGET xrpld)
install(TARGETS xrpld RUNTIME DESTINATION bin)
set_target_properties(xrpld PROPERTIES INSTALL_RPATH_USE_LINK_PATH ON)
# sample configs should not overwrite existing files
# install if-not-exists workaround as suggested by
# https://cmake.org/Bug/view.php?id=12646
install(
CODE
"
macro (copy_if_not_exists SRC DEST NEWNAME)
if (NOT EXISTS \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/\${DEST}/\${NEWNAME}\")
file (INSTALL FILE_PERMISSIONS OWNER_READ OWNER_WRITE DESTINATION \"\${CMAKE_INSTALL_PREFIX}/\${DEST}\" FILES \"\${SRC}\" RENAME \"\${NEWNAME}\")
else ()
message (\"-- Skipping : \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/\${DEST}/\${NEWNAME}\")
endif ()
endmacro()
copy_if_not_exists(\"${CMAKE_CURRENT_SOURCE_DIR}/cfg/xrpld-example.cfg\" etc xrpld.cfg)
copy_if_not_exists(\"${CMAKE_CURRENT_SOURCE_DIR}/cfg/validators-example.txt\" etc validators.txt)
"
)
install(
CODE
"
set(CMAKE_MODULE_PATH \"${CMAKE_MODULE_PATH}\")
include(create_symbolic_link)
create_symbolic_link(xrpld${suffix} \
\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/rippled${suffix})
"
)
endif()
install(
FILES
${CMAKE_CURRENT_SOURCE_DIR}/cmake/XrplConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/XrplConfigVersion.cmake
DESTINATION lib/cmake/xrpl
COMPONENT development
)

View File

@@ -50,6 +50,13 @@ if(MSVC AND CMAKE_GENERATOR_PLATFORM STREQUAL "Win32")
message(FATAL_ERROR "Visual Studio 32-bit build is not supported.")
endif()
if(voidstar AND NOT is_amd64)
message(
FATAL_ERROR
"The voidstar library only supported on amd64/x86_64. Detected archictecture was: ${CMAKE_SYSTEM_PROCESSOR}"
)
endif()
if(APPLE AND NOT HOMEBREW)
find_program(HOMEBREW brew)
endif()

View File

@@ -6,13 +6,12 @@
"sqlite3/3.49.1#8631739a4c9b93bd3d6b753bac548a63%1765850149.926",
"soci/4.0.3#a9f8d773cd33e356b5879a4b0564f287%1765850149.46",
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1765850147.878",
"secp256k1/0.7.1#481881709eb0bdd0185a12b912bbe8ad%1770910500.329",
"secp256k1/0.7.1#3a61e95e220062ef32c48d019e9c81f7%1770306721.686",
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1765850186.86",
"re2/20230301#ca3b241baec15bd31ea9187150e0b333%1765850148.103",
"protobuf/6.32.1#f481fd276fc23a33b85a3ed1e898b693%1765850161.038",
"openssl/3.5.5#05a4ac5b7323f7a329b2db1391d9941f%1770229825.601",
"openssl/3.5.5#05a4ac5b7323f7a329b2db1391d9941f%1769599205.414",
"nudb/2.0.9#0432758a24204da08fee953ec9ea03cb%1769436073.32",
"mpt-crypto/0.1.0-rc2#575de3d495f539e3e5eba957b324d260%1771955268.105",
"lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914",
"libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492",
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03",
@@ -32,7 +31,7 @@
"strawberryperl/5.32.1.1#707032463aa0620fa17ec0d887f5fe41%1765850165.196",
"protobuf/6.32.1#f481fd276fc23a33b85a3ed1e898b693%1765850161.038",
"nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1765850144.707",
"msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649",
"msys2/cci.latest#eea83308ad7e9023f7318c60d5a9e6cb%1770199879.083",
"m4/1.4.19#70dc8bbb33e981d119d2acc0175cf381%1763158052.846",
"cmake/4.2.0#ae0a44f44a1ef9ab68fd4b3e9a1f8671%1765850153.937",
"cmake/3.31.10#313d16a1aa16bbdb2ca0792467214b76%1765850153.479",

View File

@@ -31,7 +31,6 @@ class Xrpl(ConanFile):
"ed25519/2015.03",
"grpc/1.72.0",
"libarchive/3.8.1",
"mpt-crypto/0.1.0-rc2",
"nudb/2.0.9",
"openssl/3.5.5",
"secp256k1/0.7.1",
@@ -210,7 +209,6 @@ class Xrpl(ConanFile):
"grpc::grpc++",
"libarchive::libarchive",
"lz4::lz4",
"mpt-crypto::mpt-crypto",
"nudb::nudb",
"openssl::crypto",
"protobuf::libprotobuf",

View File

@@ -55,7 +55,6 @@ words:
- autobridging
- bimap
- bindir
- blindings
- bookdir
- Bougalis
- Britto
@@ -88,7 +87,6 @@ words:
- daria
- dcmake
- dearmor
- decryptor
- deleteme
- demultiplexer
- deserializaton
@@ -98,7 +96,6 @@ words:
- distro
- doxyfile
- dxrpl
- elgamal
- endmacro
- exceptioned
- Falco
@@ -107,7 +104,6 @@ words:
- fmtdur
- fsanitize
- funclets
- Gamal
- gcov
- gcovr
- ghead
@@ -193,7 +189,6 @@ words:
- partitioner
- paychan
- paychans
- Pedersen
- permdex
- perminute
- permissioned
@@ -229,7 +224,6 @@ words:
- sahyadri
- Satoshi
- scons
- Schnorr
- secp
- sendq
- seqit
@@ -256,7 +250,6 @@ words:
- stvar
- stvector
- stxchainattestations
- summands
- superpeer
- superpeers
- takergets

View File

@@ -1,484 +0,0 @@
#pragma once
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/detail/secp256k1.h>
#include <secp256k1_mpt.h>
namespace xrpl {
/**
* @brief Bundles an ElGamal public key with its associated encrypted amount.
*
* Used to represent a recipient in confidential transfers, containing both
* the recipient's ElGamal public key and the ciphertext encrypting the
* transfer amount under that key.
*/
struct ConfidentialRecipient
{
Slice publicKey; ///< The recipient's ElGamal public key (64 bytes).
Slice encryptedAmount; ///< The encrypted amount ciphertext (128 bytes).
};
/// Holds two secp256k1 public key components representing an ElGamal ciphertext (C1, C2).
struct EcPair
{
secp256k1_pubkey c1;
secp256k1_pubkey c2;
};
/**
* @brief Increments the confidential balance version counter on an MPToken.
*
* The version counter is used to prevent replay attacks by binding proofs
* to a specific state of the account's confidential balance. Wraps to 0
* on overflow (defined behavior for unsigned integers).
*
* @param mptoken The MPToken ledger entry to update.
*/
inline void
incrementConfidentialVersion(STObject& mptoken)
{
// Retrieve current version and increment.
// Unsigned integer overflow is defined behavior in C++ (wraps to 0),
// which is acceptable here.
mptoken[sfConfidentialBalanceVersion] =
mptoken[~sfConfidentialBalanceVersion].value_or(0u) + 1u;
}
/**
* @brief Adds common fields to a serializer for ZKP context hash generation.
*
* Serializes the transaction type, account, issuance ID and sequence/ticket number
* into the provided serializer. These fields form the base of all context
* hashes used in zero-knowledge proofs.
*
* @param s The serializer to append fields to.
* @param txType The transaction type identifier.
* @param account The account ID of the transaction sender.
* @param issuanceID The MPToken Issuance ID.
* @param sequence The transaction sequence number or ticket number.
*/
void
addCommonZKPFields(
Serializer& s,
std::uint16_t txType,
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID);
/**
* @brief Generates the context hash for ConfidentialMPTSend transactions.
*
* Creates a unique 256-bit hash that binds the zero-knowledge proofs to
* this specific send transaction, preventing proof reuse across transactions.
*
* @param account The sender's account ID.
* @param issuanceID The MPToken Issuance ID.
* @param sequence The transaction sequence number or ticket number.
* @param destination The destination account ID.
* @param version The sender's confidential balance version.
* @return A 256-bit context hash unique to this transaction.
*/
uint256
getSendContextHash(
AccountID const& account,
uint192 const& issuanceID,
std::uint32_t sequence,
AccountID const& destination,
std::uint32_t version);
/**
* @brief Generates the context hash for ConfidentialMPTClawback transactions.
*
* Creates a unique 256-bit hash that binds the equality proof to this
* specific clawback transaction.
*
* @param account The issuer's account ID.
* @param issuanceID The MPToken Issuance ID.
* @param sequence The transaction sequence number or ticket number.
* @param holder The holder's account ID being clawed back from.
* @return A 256-bit context hash unique to this transaction.
*/
uint256
getClawbackContextHash(
AccountID const& account,
uint192 const& issuanceID,
std::uint32_t sequence,
AccountID const& holder);
/**
* @brief Generates the context hash for ConfidentialMPTConvert transactions.
*
* Creates a unique 256-bit hash that binds the Schnorr proof (for key
* registration) to this specific convert transaction.
*
* @param account The holder's account ID.
* @param issuanceID The MPToken Issuance ID.
* @param sequence The transaction sequence number or a ticket number.
* @return A 256-bit context hash unique to this transaction.
*/
uint256
getConvertContextHash(AccountID const& account, uint192 const& issuanceID, std::uint32_t sequence);
/**
* @brief Generates the context hash for ConfidentialMPTConvertBack transactions.
*
* Creates a unique 256-bit hash that binds the zero-knowledge proofs to
* this specific convert-back transaction.
*
* @param account The holder's account ID.
* @param issuanceID The MPToken Issuance ID.
* @param sequence The transaction sequence number or a ticket number.
* @param version The holder's confidential balance version.
* @return A 256-bit context hash unique to this transaction.
*/
uint256
getConvertBackContextHash(
AccountID const& account,
uint192 const& issuanceID,
std::uint32_t sequence,
std::uint32_t version);
/**
* @brief Parses an ElGamal ciphertext into two secp256k1 public key components.
*
* Breaks a 66-byte encrypted amount (two 33-byte compressed EC points) into
* a pair containing (C1, C2) for use in cryptographic operations.
*
* @param buffer The 66-byte buffer containing the compressed ciphertext.
* @return The parsed pair (c1, c2) if successful, std::nullopt if the buffer is invalid.
*/
std::optional<EcPair>
makeEcPair(Slice const& buffer);
/**
* @brief Serializes an EcPair into compressed form.
*
* Converts an EcPair (C1, C2) back into a 66-byte buffer containing
* two 33-byte compressed EC points.
*
* @param pair The EcPair to serialize.
* @return The 66-byte buffer, or std::nullopt if serialization fails.
*/
std::optional<Buffer>
serializeEcPair(EcPair const& pair);
/**
* @brief Verifies that a buffer contains two valid, parsable EC public keys.
*
* @param buffer The input buffer containing two concatenated components.
* @return true if both components can be parsed successfully, false otherwise.
*/
bool
isValidCiphertext(Slice const& buffer);
/**
* @brief Verifies that a buffer contains a valid, parsable compressed EC point.
*
* Can be used to validate both compressed public keys and Pedersen commitments.
* Fails early if the prefix byte is not 0x02 or 0x03.
*
* @param buffer The input buffer containing a compressed EC point (33 bytes).
* @return true if the point can be parsed successfully, false otherwise.
*/
bool
isValidCompressedECPoint(Slice const& buffer);
/**
* @brief Homomorphically adds two ElGamal ciphertexts.
*
* Uses the additive homomorphic property of ElGamal encryption to compute
* Enc(a + b) from Enc(a) and Enc(b) without decryption.
*
* @param a The first ciphertext (66 bytes).
* @param b The second ciphertext (66 bytes).
* @return The resulting ciphertext Enc(a + b), or std::nullopt on failure.
*/
std::optional<Buffer>
homomorphicAdd(Slice const& a, Slice const& b);
/**
* @brief Homomorphically subtracts two ElGamal ciphertexts.
*
* Uses the additive homomorphic property of ElGamal encryption to compute
* Enc(a - b) from Enc(a) and Enc(b) without decryption.
*
* @param a The minuend ciphertext (66 bytes).
* @param b The subtrahend ciphertext (66 bytes).
* @return The resulting ciphertext Enc(a - b), or std::nullopt on failure.
*/
std::optional<Buffer>
homomorphicSubtract(Slice const& a, Slice const& b);
/**
* @brief Encrypts an amount using ElGamal encryption.
*
* Produces a ciphertext C = (C1, C2) where C1 = r*G and C2 = m*G + r*Pk,
* using the provided blinding factor r.
*
* @param amt The plaintext amount to encrypt.
* @param pubKeySlice The recipient's ElGamal public key (64 bytes).
* @param blindingFactor The 32-byte randomness used as blinding factor r.
* @return The 66-byte ciphertext, or std::nullopt on failure.
*/
std::optional<Buffer>
encryptAmount(uint64_t const amt, Slice const& pubKeySlice, Slice const& blindingFactor);
/**
* @brief Generates the canonical zero encryption for a specific MPToken.
*
* Creates a deterministic encryption of zero that is unique to the account
* and MPT issuance. Used to initialize confidential balance fields.
*
* @param pubKeySlice The holder's ElGamal public key (64 bytes).
* @param account The account ID of the token holder.
* @param mptId The MPToken Issuance ID.
* @return The 66-byte canonical zero ciphertext, or std::nullopt on failure.
*/
std::optional<Buffer>
encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId);
/**
* @brief Verifies a Schnorr proof of knowledge of an ElGamal private key.
*
* Proves that the submitter knows the secret key corresponding to the
* provided public key, without revealing the secret key itself.
*
* @param pubKeySlice The ElGamal public key (64 bytes).
* @param proofSlice The Schnorr proof (65 bytes).
* @param contextHash The 256-bit context hash binding the proof.
* @return tesSUCCESS if valid, or an error code otherwise.
*/
TER
verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash);
/**
* @brief Verifies that a ciphertext correctly encrypts a revealed amount.
*
* Given the plaintext amount and blinding factor, verifies that the
* ciphertext was correctly constructed using ElGamal encryption.
*
* @param amount The revealed plaintext amount.
* @param blindingFactor The 32-byte blinding factor used in encryption.
* @param pubKeySlice The recipient's ElGamal public key (64 bytes).
* @param ciphertext The ciphertext to verify (66 bytes).
* @return tesSUCCESS if the encryption is valid, or an error code otherwise.
*/
TER
verifyElGamalEncryption(
uint64_t const amount,
Slice const& blindingFactor,
Slice const& pubKeySlice,
Slice const& ciphertext);
/**
* @brief Validates the format of encrypted amount fields in a transaction.
*
* Checks that all ciphertext fields in the transaction object have the
* correct length and contain valid EC points. This function is only used
* by ConfidentialMPTConvert and ConfidentialMPTConvertBack transactions.
*
* @param object The transaction object containing encrypted amount fields.
* @return tesSUCCESS if all formats are valid, temMALFORMED if required fields
* are missing, or temBAD_CIPHERTEXT if format validation fails.
*/
NotTEC
checkEncryptedAmountFormat(STObject const& object);
/**
* @brief Verifies revealed amount encryptions for all recipients.
*
* Validates that the same amount was correctly encrypted for the holder,
* issuer, and optionally the auditor using their respective public keys.
*
* @param amount The revealed plaintext amount.
* @param blindingFactor The 32-byte blinding factor used in all encryptions.
* @param holder The holder's public key and encrypted amount.
* @param issuer The issuer's public key and encrypted amount.
* @param auditor Optional auditor's public key and encrypted amount.
* @return tesSUCCESS if all encryptions are valid, or an error code otherwise.
*/
TER
verifyRevealedAmount(
uint64_t const amount,
Slice const& blindingFactor,
ConfidentialRecipient const& holder,
ConfidentialRecipient const& issuer,
std::optional<ConfidentialRecipient> const& auditor);
/**
* @brief Returns the number of recipients in a confidential transfer.
*
* Returns 4 if an auditor is present (sender, destination, issuer, auditor),
* or 3 if no auditor (sender, destination, issuer).
*
* @param hasAuditor Whether the issuance has an auditor configured.
* @return The number of recipients (3 or 4).
*/
constexpr std::size_t
getConfidentialRecipientCount(bool hasAuditor)
{
return hasAuditor ? 4 : 3;
}
/**
* @brief Verifies a multi-ciphertext equality proof.
*
* Proves that all ciphertexts in the recipients vector encrypt the same
* plaintext amount, without revealing the amount itself.
*
* @param proof The zero-knowledge proof bytes.
* @param recipients Vector of recipients with their public keys and ciphertexts.
* @param nRecipients The number of recipients (must match recipients.size()).
* @param contextHash The 256-bit context hash binding the proof.
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
*/
TER
verifyMultiCiphertextEqualityProof(
Slice const& proof,
std::vector<ConfidentialRecipient> const& recipients,
std::size_t const nRecipients,
uint256 const& contextHash);
/**
* @brief Verifies a clawback equality proof.
*
* Proves that the issuer knows the exact amount encrypted in the holder's
* balance ciphertext. Used in ConfidentialMPTClawback to verify the issuer
* can decrypt the balance using their private key.
*
* @param amount The revealed plaintext amount.
* @param proof The zero-knowledge proof bytes.
* @param pubKeySlice The issuer's ElGamal public key (64 bytes).
* @param ciphertext The issuer's encrypted balance on the holder's account (66 bytes).
* @param contextHash The 256-bit context hash binding the proof.
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
*/
TER
verifyClawbackEqualityProof(
uint64_t const amount,
Slice const& proof,
Slice const& pubKeySlice,
Slice const& ciphertext,
uint256 const& contextHash);
/**
* @brief Generates a cryptographically secure 32-byte blinding factor.
*
* Produces random bytes suitable for use as an ElGamal blinding factor
* or Pedersen commitment randomness.
*
* @return A 32-byte buffer containing the random blinding factor.
*/
Buffer
generateBlindingFactor();
/**
* @brief Verifies the cryptographic link between an ElGamal Ciphertext and a
* Pedersen Commitment for a transaction Amount.
*
* It proves that the ElGamal ciphertext `encAmt` encrypts the same value `m`
* as the Pedersen Commitment `pcmSlice`, using the randomness `r`.
* Proves Enc(m) <-> Pcm(m)
*
* @param proof The Zero Knowledge Proof bytes.
* @param encAmt The ElGamal ciphertext of the amount (C1, C2).
* @param pubKeySlice The sender's public key.
* @param pcmSlice The Pedersen Commitment to the amount.
* @param contextHash The unique context hash for this transaction.
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
*/
TER
verifyAmountPcmLinkage(
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,
Slice const& pcmSlice,
uint256 const& contextHash);
/**
* @brief Verifies the cryptographic link between an ElGamal Ciphertext and a
* Pedersen Commitment for an account Balance.
*
* It proves that the ElGamal ciphertext `encAmt` encrypts the same value `b`
* as the Pedersen Commitment `pcmSlice`, using the secret key `s`.
* Proves Enc(b) <-> Pcm(b)
*
* Note: Swaps arguments (Pk <-> C1) to accommodate the different algebraic
* structure.
*
* @param proof The Zero Knowledge Proof bytes.
* @param encAmt The ElGamal ciphertext of the balance (C1, C2).
* @param pubKeySlice The sender's public key.
* @param pcmSlice The Pedersen Commitment to the balance.
* @param contextHash The unique context hash for this transaction.
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
*/
TER
verifyBalancePcmLinkage(
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,
Slice const& pcmSlice,
uint256 const& contextHash);
/**
* @brief Verifies an aggregated Bulletproof range proof.
*
* This function verifies that all commitments in commitment_C_vec commit
* to values within the valid 64-bit range [0, 2^64 - 1].
*
* @param proof The serialized Bulletproof proof.
* @param compressedCommitments Vector of compressed Pedersen commitments (each 33 bytes).
* @param contextHash The unique context hash for this transaction.
* @return tesSUCCESS if the proof is valid, tecBAD_PROOF if verification
* fails, or tecINTERNAL for internal errors.
*/
TER
verifyAggregatedBulletproof(
Slice const& proof,
std::vector<Slice> const& compressedCommitments,
uint256 const& contextHash);
/**
* @brief Computes the remainder commitment for ConfidentialMPTSend.
*
* Given a balance commitment PC_bal = m_bal*G + rho_bal*H and an amount
* commitment PC_amt = m_amt*G + rho_amt*H, this function computes:
* PC_rem = PC_bal - PC_amt = (m_bal - m_amt)*G + (rho_bal - rho_amt)*H
*
* This derived commitment is used in an aggregated range proof to ensure
* the sender maintains a non-negative balance (m_bal - m_amt >= 0).
*
* @param balanceCommitment The compressed Pedersen commitment to the balance (33 bytes).
* @param amountCommitment The compressed Pedersen commitment to the amount (33 bytes).
* @param out Output buffer for the resulting remainder commitment (33 bytes).
* @return tesSUCCESS on success, tecINTERNAL on failure.
*/
TER
computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment, Buffer& out);
/**
* @brief Computes the remainder commitment for ConvertBack.
*
* Given a Pedersen commitment PC = m*G + rho*H, this function computes
* PC_rem = PC - amount*G = (m - amount)*G + rho*H
*
* @param commitment The compressed Pedersen commitment (33 bytes).
* @param amount The amount to subtract (must be non-zero).
* @param out Output buffer for the resulting commitment (33 bytes).
* @return tesSUCCESS on success, tecINTERNAL on failure or if amount is 0.
*/
TER
computeConvertBackRemainder(Slice const& commitment, uint64_t amount, Buffer& out);
} // namespace xrpl

View File

@@ -173,8 +173,7 @@ enum LedgerEntryType : std::uint16_t {
LSF_FLAG(lsfMPTCanEscrow, 0x00000008) \
LSF_FLAG(lsfMPTCanTrade, 0x00000010) \
LSF_FLAG(lsfMPTCanTransfer, 0x00000020) \
LSF_FLAG(lsfMPTCanClawback, 0x00000040) \
LSF_FLAG(lsfMPTCanConfidentialAmount, 0x00000080)) \
LSF_FLAG(lsfMPTCanClawback, 0x00000040)) \
\
LEDGER_OBJECT(MPTokenIssuanceMutable, \
LSF_FLAG(lsmfMPTCanMutateCanLock, 0x00000002) \
@@ -184,8 +183,7 @@ enum LedgerEntryType : std::uint16_t {
LSF_FLAG(lsmfMPTCanMutateCanTransfer, 0x00000020) \
LSF_FLAG(lsmfMPTCanMutateCanClawback, 0x00000040) \
LSF_FLAG(lsmfMPTCanMutateMetadata, 0x00010000) \
LSF_FLAG(lsmfMPTCanMutateTransferFee, 0x00020000) \
LSF_FLAG(lsmfMPTCannotMutateCanConfidentialAmount, 0x00040000)) \
LSF_FLAG(lsmfMPTCanMutateTransferFee, 0x00020000)) \
\
LEDGER_OBJECT(MPToken, \
LSF_FLAG2(lsfMPTLocked, 0x00000001) \

View File

@@ -307,49 +307,4 @@ std::size_t constexpr permissionMaxSize = 10;
/** The maximum number of transactions that can be in a batch. */
std::size_t constexpr maxBatchTxCount = 8;
/** EC ElGamal ciphertext length 33-byte */
std::size_t constexpr ecGamalEncryptedLength = 33;
/** EC ElGamal ciphertext length: two 33-byte components concatenated */
std::size_t constexpr ecGamalEncryptedTotalLength = ecGamalEncryptedLength * 2;
/** Length of equality ZKProof in bytes */
std::size_t constexpr ecEqualityProofLength = 98;
/** Length of EC point (compressed) */
std::size_t constexpr compressedECPointLength = 33;
/** Length of EC public key (compressed) */
std::size_t constexpr ecPubKeyLength = compressedECPointLength;
/** Length of EC private key in bytes */
std::size_t constexpr ecPrivKeyLength = 32;
/** Length of the EC blinding factor in bytes */
std::size_t constexpr ecBlindingFactorLength = 32;
/** Length of Schnorr ZKProof for public key registration in bytes */
std::size_t constexpr ecSchnorrProofLength = 65;
/** Length of ElGamal ciphertext equality proof in bytes */
std::size_t constexpr ecCiphertextEqualityProofLength = 261;
/** Length of ElGamal Pedersen linkage proof in bytes */
std::size_t constexpr ecPedersenProofLength = 195;
/** Length of Pedersen Commitment (compressed) */
std::size_t constexpr ecPedersenCommitmentLength = compressedECPointLength;
/** Length of single bulletproof (range proof for 1 commitment) in bytes */
std::size_t constexpr ecSingleBulletproofLength = 688;
/** Length of double bulletproof (range proof for 2 commitments) in bytes */
std::size_t constexpr ecDoubleBulletproofLength = 754;
/** Compressed EC point prefix for even y-coordinate */
static constexpr std::uint8_t ecCompressedPrefixEvenY = 0x02;
/** Compressed EC point prefix for odd y-coordinate */
static constexpr std::uint8_t ecCompressedPrefixOddY = 0x03;
} // namespace xrpl

View File

@@ -23,9 +23,10 @@ struct JsonOptions
none = 0b0000'0000,
include_date = 0b0000'0001,
disable_API_prior_V2 = 0b0000'0010,
disable_API_prior_V3 = 0b0000'0100,
// IMPORTANT `_all` must be union of all of the above; see also operator~
_all = 0b0000'0011
_all = 0b0000'0111
// clang-format on
};

View File

@@ -121,7 +121,6 @@ enum TEMcodes : TERUnderlyingType {
temARRAY_TOO_LARGE,
temBAD_TRANSFER_FEE,
temINVALID_INNER_BATCH,
temBAD_CIPHERTEXT,
};
//------------------------------------------------------------------------------
@@ -347,7 +346,6 @@ enum TECcodes : TERUnderlyingType {
// backward compatibility with historical data on non-prod networks, can be
// reclaimed after those networks reset.
tecNO_DELEGATE_PERMISSION = 198,
tecBAD_PROOF = 199,
};
//------------------------------------------------------------------------------

View File

@@ -138,8 +138,7 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
TF_FLAG(tfMPTCanEscrow, lsfMPTCanEscrow) \
TF_FLAG(tfMPTCanTrade, lsfMPTCanTrade) \
TF_FLAG(tfMPTCanTransfer, lsfMPTCanTransfer) \
TF_FLAG(tfMPTCanClawback, lsfMPTCanClawback) \
TF_FLAG(tfMPTCanConfidentialAmount, lsfMPTCanConfidentialAmount), \
TF_FLAG(tfMPTCanClawback, lsfMPTCanClawback), \
MASK_ADJ(0)) \
\
TRANSACTION(MPTokenAuthorize, \
@@ -348,12 +347,10 @@ inline constexpr FlagValue tmfMPTCanMutateCanTransfer = lsmfMPTCanMutateCanTrans
inline constexpr FlagValue tmfMPTCanMutateCanClawback = lsmfMPTCanMutateCanClawback;
inline constexpr FlagValue tmfMPTCanMutateMetadata = lsmfMPTCanMutateMetadata;
inline constexpr FlagValue tmfMPTCanMutateTransferFee = lsmfMPTCanMutateTransferFee;
inline constexpr FlagValue tmfMPTCannotMutateCanConfidentialAmount =
lsmfMPTCannotMutateCanConfidentialAmount;
inline constexpr FlagValue tmfMPTokenIssuanceCreateMutableMask = ~(
tmfMPTCanMutateCanLock | tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanEscrow |
tmfMPTCanMutateCanTrade | tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanClawback |
tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee | tmfMPTCannotMutateCanConfidentialAmount);
inline constexpr FlagValue tmfMPTokenIssuanceCreateMutableMask =
~(tmfMPTCanMutateCanLock | tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanEscrow |
tmfMPTCanMutateCanTrade | tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanClawback |
tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee);
// MPTokenIssuanceSet MutableFlags:
// Set or Clear flags.
@@ -370,13 +367,10 @@ inline constexpr FlagValue tmfMPTSetCanTransfer = 0x00000100;
inline constexpr FlagValue tmfMPTClearCanTransfer = 0x00000200;
inline constexpr FlagValue tmfMPTSetCanClawback = 0x00000400;
inline constexpr FlagValue tmfMPTClearCanClawback = 0x00000800;
inline constexpr FlagValue tmfMPTSetCanConfidentialAmount = 0x00001000;
inline constexpr FlagValue tmfMPTClearCanConfidentialAmount = 0x00002000;
inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask =
~(tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTSetRequireAuth | tmfMPTClearRequireAuth |
tmfMPTSetCanEscrow | tmfMPTClearCanEscrow | tmfMPTSetCanTrade | tmfMPTClearCanTrade |
tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | tmfMPTSetCanClawback |
tmfMPTClearCanClawback | tmfMPTSetCanConfidentialAmount | tmfMPTClearCanConfidentialAmount);
inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask = ~(
tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTSetRequireAuth | tmfMPTClearRequireAuth |
tmfMPTSetCanEscrow | tmfMPTClearCanEscrow | tmfMPTSetCanTrade | tmfMPTClearCanTrade |
tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | tmfMPTSetCanClawback | tmfMPTClearCanClawback);
// Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between accounts allowed a
// TrustLine to be added to the issuer of that token without explicit permission from that issuer.

View File

@@ -15,7 +15,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FEATURE(ConfidentialTransfer, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (ExpiredNFTokenOfferRemoval, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (BatchInnerSigs, Supported::no, VoteBehavior::DefaultNo)

View File

@@ -385,41 +385,32 @@ LEDGER_ENTRY(ltAMM, 0x0079, AMM, amm, ({
\sa keylet::mptIssuance
*/
LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
{sfIssuer, soeREQUIRED},
{sfSequence, soeREQUIRED},
{sfTransferFee, soeDEFAULT},
{sfOwnerNode, soeREQUIRED},
{sfAssetScale, soeDEFAULT},
{sfMaximumAmount, soeOPTIONAL},
{sfOutstandingAmount, soeREQUIRED},
{sfLockedAmount, soeOPTIONAL},
{sfMPTokenMetadata, soeOPTIONAL},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfDomainID, soeOPTIONAL},
{sfMutableFlags, soeDEFAULT},
{sfIssuerEncryptionKey, soeOPTIONAL},
{sfAuditorEncryptionKey, soeOPTIONAL},
{sfConfidentialOutstandingAmount, soeDEFAULT},
{sfIssuer, soeREQUIRED},
{sfSequence, soeREQUIRED},
{sfTransferFee, soeDEFAULT},
{sfOwnerNode, soeREQUIRED},
{sfAssetScale, soeDEFAULT},
{sfMaximumAmount, soeOPTIONAL},
{sfOutstandingAmount, soeREQUIRED},
{sfLockedAmount, soeOPTIONAL},
{sfMPTokenMetadata, soeOPTIONAL},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfDomainID, soeOPTIONAL},
{sfMutableFlags, soeDEFAULT},
}))
/** A ledger object which tracks MPToken
\sa keylet::mptoken
*/
LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
{sfAccount, soeREQUIRED},
{sfMPTokenIssuanceID, soeREQUIRED},
{sfMPTAmount, soeDEFAULT},
{sfLockedAmount, soeOPTIONAL},
{sfOwnerNode, soeREQUIRED},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfConfidentialBalanceInbox, soeOPTIONAL},
{sfConfidentialBalanceSpending, soeOPTIONAL},
{sfConfidentialBalanceVersion, soeDEFAULT},
{sfIssuerEncryptedBalance, soeOPTIONAL},
{sfAuditorEncryptedBalance, soeOPTIONAL},
{sfHolderEncryptionKey, soeOPTIONAL},
{sfAccount, soeREQUIRED},
{sfMPTokenIssuanceID, soeREQUIRED},
{sfMPTAmount, soeDEFAULT},
{sfLockedAmount, soeOPTIONAL},
{sfOwnerNode, soeREQUIRED},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
/** A ledger object which tracks Oracle

View File

@@ -114,7 +114,6 @@ TYPED_SFIELD(sfInterestRate, UINT32, 65) // 1/10 basis points (bi
TYPED_SFIELD(sfLateInterestRate, UINT32, 66) // 1/10 basis points (bips)
TYPED_SFIELD(sfCloseInterestRate, UINT32, 67) // 1/10 basis points (bips)
TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bips)
TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32, 69)
// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)
@@ -148,7 +147,6 @@ TYPED_SFIELD(sfSubjectNode, UINT64, 28)
TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::sMD_BaseTen|SField::sMD_Default)
TYPED_SFIELD(sfVaultNode, UINT64, 30)
TYPED_SFIELD(sfLoanBrokerNode, UINT64, 31)
TYPED_SFIELD(sfConfidentialOutstandingAmount, UINT64, 32, SField::sMD_BaseTen|SField::sMD_Default)
// 128-bit
TYPED_SFIELD(sfEmailHash, UINT128, 1)
@@ -299,22 +297,6 @@ TYPED_SFIELD(sfAssetClass, VL, 28)
TYPED_SFIELD(sfProvider, VL, 29)
TYPED_SFIELD(sfMPTokenMetadata, VL, 30)
TYPED_SFIELD(sfCredentialType, VL, 31)
TYPED_SFIELD(sfConfidentialBalanceInbox, VL, 32)
TYPED_SFIELD(sfConfidentialBalanceSpending, VL, 33)
TYPED_SFIELD(sfIssuerEncryptedBalance, VL, 34)
TYPED_SFIELD(sfIssuerEncryptionKey, VL, 35)
TYPED_SFIELD(sfHolderEncryptionKey, VL, 36)
TYPED_SFIELD(sfZKProof, VL, 37)
TYPED_SFIELD(sfHolderEncryptedAmount, VL, 38)
TYPED_SFIELD(sfIssuerEncryptedAmount, VL, 39)
TYPED_SFIELD(sfSenderEncryptedAmount, VL, 40)
TYPED_SFIELD(sfDestinationEncryptedAmount, VL, 41)
TYPED_SFIELD(sfAuditorEncryptedBalance, VL, 42)
TYPED_SFIELD(sfAuditorEncryptedAmount, VL, 43)
TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44)
TYPED_SFIELD(sfBlindingFactor, VL, 45)
TYPED_SFIELD(sfAmountCommitment, VL, 46)
TYPED_SFIELD(sfBalanceCommitment, VL, 47)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)

View File

@@ -42,7 +42,7 @@ TRANSACTION(ttPAYMENT, 0, Payment,
/** This transaction type creates an escrow object. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/escrow/Escrow.h>
# include <xrpl/tx/transactors/escrow/EscrowCreate.h>
#endif
TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate,
Delegation::delegable,
@@ -58,6 +58,9 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate,
}))
/** This transaction type completes an existing escrow. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/escrow/EscrowFinish.h>
#endif
TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish,
Delegation::delegable,
uint256{},
@@ -94,7 +97,7 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet,
/** This transaction type cancels an existing escrow. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/escrow/Escrow.h>
# include <xrpl/tx/transactors/escrow/EscrowCancel.h>
#endif
TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel,
Delegation::delegable,
@@ -180,7 +183,7 @@ TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet,
/** This transaction type creates a new unidirectional XRP payment channel. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/payment_channel/PayChan.h>
# include <xrpl/tx/transactors/payment_channel/PayChanCreate.h>
#endif
TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate,
Delegation::delegable,
@@ -196,6 +199,9 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate,
}))
/** This transaction type funds an existing unidirectional XRP payment channel. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/payment_channel/PayChanFund.h>
#endif
TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund,
Delegation::delegable,
uint256{},
@@ -207,6 +213,9 @@ TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund,
}))
/** This transaction type submits a claim against an existing unidirectional payment channel. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/payment_channel/PayChanClaim.h>
#endif
TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim,
Delegation::delegable,
uint256{},
@@ -617,7 +626,7 @@ TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge,
/** This transaction type creates or updates a DID */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/did/DID.h>
# include <xrpl/tx/transactors/did/DIDSet.h>
#endif
TRANSACTION(ttDID_SET, 49, DIDSet,
Delegation::delegable,
@@ -630,6 +639,9 @@ TRANSACTION(ttDID_SET, 49, DIDSet,
}))
/** This transaction type deletes a DID */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/did/DIDDelete.h>
#endif
TRANSACTION(ttDID_DELETE, 50, DIDDelete,
Delegation::delegable,
featureDID,
@@ -722,8 +734,6 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet,
{sfMPTokenMetadata, soeOPTIONAL},
{sfTransferFee, soeOPTIONAL},
{sfMutableFlags, soeOPTIONAL},
{sfIssuerEncryptionKey, soeOPTIONAL},
{sfAuditorEncryptionKey, soeOPTIONAL},
}))
/** This transaction type authorizes a MPToken instance */
@@ -741,7 +751,7 @@ TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize,
/** This transaction type create an Credential instance */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/credentials/Credentials.h>
# include <xrpl/tx/transactors/credentials/CredentialCreate.h>
#endif
TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate,
Delegation::delegable,
@@ -755,6 +765,9 @@ TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate,
}))
/** This transaction type accept an Credential object */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/credentials/CredentialAccept.h>
#endif
TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept,
Delegation::delegable,
featureCredentials,
@@ -765,6 +778,9 @@ TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept,
}))
/** This transaction type delete an Credential object */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/credentials/CredentialDelete.h>
#endif
TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete,
Delegation::delegable,
featureCredentials,
@@ -832,7 +848,7 @@ TRANSACTION(ttDELEGATE_SET, 64, DelegateSet,
# include <xrpl/tx/transactors/vault/VaultCreate.h>
#endif
TRANSACTION(ttVAULT_CREATE, 65, VaultCreate,
Delegation::delegable,
Delegation::notDelegable,
featureSingleAssetVault,
createPseudoAcct | createMPTIssuance | mustModifyVault,
({
@@ -850,7 +866,7 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate,
# include <xrpl/tx/transactors/vault/VaultSet.h>
#endif
TRANSACTION(ttVAULT_SET, 66, VaultSet,
Delegation::delegable,
Delegation::notDelegable,
featureSingleAssetVault,
mustModifyVault,
({
@@ -865,7 +881,7 @@ TRANSACTION(ttVAULT_SET, 66, VaultSet,
# include <xrpl/tx/transactors/vault/VaultDelete.h>
#endif
TRANSACTION(ttVAULT_DELETE, 67, VaultDelete,
Delegation::delegable,
Delegation::notDelegable,
featureSingleAssetVault,
mustDeleteAcct | destroyMPTIssuance | mustModifyVault,
({
@@ -877,7 +893,7 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete,
# include <xrpl/tx/transactors/vault/VaultDeposit.h>
#endif
TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit,
Delegation::delegable,
Delegation::notDelegable,
featureSingleAssetVault,
mayAuthorizeMPT | mustModifyVault,
({
@@ -890,7 +906,7 @@ TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit,
# include <xrpl/tx/transactors/vault/VaultWithdraw.h>
#endif
TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw,
Delegation::delegable,
Delegation::notDelegable,
featureSingleAssetVault,
mayDeleteMPT | mayAuthorizeMPT | mustModifyVault,
({
@@ -905,7 +921,7 @@ TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw,
# include <xrpl/tx/transactors/vault/VaultClawback.h>
#endif
TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback,
Delegation::delegable,
Delegation::notDelegable,
featureSingleAssetVault,
mayDeleteMPT | mustModifyVault,
({
@@ -934,7 +950,7 @@ TRANSACTION(ttBATCH, 71, Batch,
# include <xrpl/tx/transactors/lending/LoanBrokerSet.h>
#endif
TRANSACTION(ttLOAN_BROKER_SET, 74, LoanBrokerSet,
Delegation::delegable,
Delegation::notDelegable,
featureLendingProtocol,
createPseudoAcct | mayAuthorizeMPT, ({
{sfVaultID, soeREQUIRED},
@@ -951,7 +967,7 @@ TRANSACTION(ttLOAN_BROKER_SET, 74, LoanBrokerSet,
# include <xrpl/tx/transactors/lending/LoanBrokerDelete.h>
#endif
TRANSACTION(ttLOAN_BROKER_DELETE, 75, LoanBrokerDelete,
Delegation::delegable,
Delegation::notDelegable,
featureLendingProtocol,
mustDeleteAcct | mayAuthorizeMPT, ({
{sfLoanBrokerID, soeREQUIRED},
@@ -962,7 +978,7 @@ TRANSACTION(ttLOAN_BROKER_DELETE, 75, LoanBrokerDelete,
# include <xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h>
#endif
TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit,
Delegation::delegable,
Delegation::notDelegable,
featureLendingProtocol,
noPriv, ({
{sfLoanBrokerID, soeREQUIRED},
@@ -974,7 +990,7 @@ TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit,
# include <xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h>
#endif
TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw,
Delegation::delegable,
Delegation::notDelegable,
featureLendingProtocol,
mayAuthorizeMPT, ({
{sfLoanBrokerID, soeREQUIRED},
@@ -989,7 +1005,7 @@ TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw,
# include <xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h>
#endif
TRANSACTION(ttLOAN_BROKER_COVER_CLAWBACK, 78, LoanBrokerCoverClawback,
Delegation::delegable,
Delegation::notDelegable,
featureLendingProtocol,
noPriv, ({
{sfLoanBrokerID, soeOPTIONAL},
@@ -1001,7 +1017,7 @@ TRANSACTION(ttLOAN_BROKER_COVER_CLAWBACK, 78, LoanBrokerCoverClawback,
# include <xrpl/tx/transactors/lending/LoanSet.h>
#endif
TRANSACTION(ttLOAN_SET, 80, LoanSet,
Delegation::delegable,
Delegation::notDelegable,
featureLendingProtocol,
mayAuthorizeMPT | mustModifyVault, ({
{sfLoanBrokerID, soeREQUIRED},
@@ -1028,7 +1044,7 @@ TRANSACTION(ttLOAN_SET, 80, LoanSet,
# include <xrpl/tx/transactors/lending/LoanDelete.h>
#endif
TRANSACTION(ttLOAN_DELETE, 81, LoanDelete,
Delegation::delegable,
Delegation::notDelegable,
featureLendingProtocol,
noPriv, ({
{sfLoanID, soeREQUIRED},
@@ -1039,7 +1055,7 @@ TRANSACTION(ttLOAN_DELETE, 81, LoanDelete,
# include <xrpl/tx/transactors/lending/LoanManage.h>
#endif
TRANSACTION(ttLOAN_MANAGE, 82, LoanManage,
Delegation::delegable,
Delegation::notDelegable,
featureLendingProtocol,
// All of the LoanManage options will modify the vault, but the
// transaction can succeed without options, essentially making it
@@ -1053,97 +1069,13 @@ TRANSACTION(ttLOAN_MANAGE, 82, LoanManage,
# include <xrpl/tx/transactors/lending/LoanPay.h>
#endif
TRANSACTION(ttLOAN_PAY, 84, LoanPay,
Delegation::delegable,
Delegation::notDelegable,
featureLendingProtocol,
mayAuthorizeMPT | mustModifyVault, ({
{sfLoanID, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
}))
/** This transaction type converts into confidential MPT balance. */
#if TRANSACTION_INCLUDE
#include <xrpl/tx/transactors/token/ConfidentialMPTConvert.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT, 85, ConfidentialMPTConvert,
Delegation::delegable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfMPTAmount, soeREQUIRED},
{sfHolderEncryptionKey, soeOPTIONAL},
{sfHolderEncryptedAmount, soeREQUIRED},
{sfIssuerEncryptedAmount, soeREQUIRED},
{sfAuditorEncryptedAmount, soeOPTIONAL},
{sfBlindingFactor, soeREQUIRED},
{sfZKProof, soeOPTIONAL},
}))
/** This transaction type merges MPT inbox. */
#if TRANSACTION_INCLUDE
#include <xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_MERGE_INBOX, 86, ConfidentialMPTMergeInbox,
Delegation::delegable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
}))
/** This transaction type converts back into public MPT balance. */
#if TRANSACTION_INCLUDE
#include <xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT_BACK, 87, ConfidentialMPTConvertBack,
Delegation::delegable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfMPTAmount, soeREQUIRED},
{sfHolderEncryptedAmount, soeREQUIRED},
{sfIssuerEncryptedAmount, soeREQUIRED},
{sfAuditorEncryptedAmount, soeOPTIONAL},
{sfBlindingFactor, soeREQUIRED},
{sfZKProof, soeREQUIRED},
{sfBalanceCommitment, soeREQUIRED},
}))
#if TRANSACTION_INCLUDE
#include <xrpl/tx/transactors/token/ConfidentialMPTSend.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_SEND, 88, ConfidentialMPTSend,
Delegation::delegable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfDestination, soeREQUIRED},
{sfSenderEncryptedAmount, soeREQUIRED},
{sfDestinationEncryptedAmount, soeREQUIRED},
{sfIssuerEncryptedAmount, soeREQUIRED},
{sfAuditorEncryptedAmount, soeOPTIONAL},
{sfZKProof, soeREQUIRED},
{sfAmountCommitment, soeREQUIRED},
{sfBalanceCommitment, soeREQUIRED},
{sfCredentialIDs, soeOPTIONAL},
}))
#if TRANSACTION_INCLUDE
#include <xrpl/tx/transactors/token/ConfidentialMPTClawback.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback,
Delegation::delegable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfHolder, soeREQUIRED},
{sfMPTAmount, soeREQUIRED},
{sfZKProof, soeREQUIRED},
}))
/** This system-generated transaction type is used to update the status of the various amendments.
For details, see: https://xrpl.org/amendments.html

View File

@@ -366,8 +366,7 @@ using InvariantChecks = std::tuple<
ValidPseudoAccounts,
ValidLoanBroker,
ValidLoan,
ValidVault,
ValidConfidentialMPToken>;
ValidVault>;
/**
* @brief get a tuple of all invariant checks

View File

@@ -28,47 +28,4 @@ public:
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariants: Confidential MPToken consistency
*
* - Convert/ConvertBack symmetry:
* Regular MPToken balance change (±X) == COA (Confidential Outstanding Amount) change (∓X)
* - Cannot delete MPToken with non-zero confidential state:
* Cannot delete if sfIssuerEncryptedBalance exists
* Cannot delete if sfConfidentialBalanceInbox and sfConfidentialBalanceSpending exist
* - Privacy flag consistency:
* MPToken can only have encrypted fields if lsfMPTCanConfidentialAmount is set on
* issuance.
* - Encrypted field existence consistency:
* If sfConfidentialBalanceSpending/sfConfidentialBalanceInbox exists, then
* sfIssuerEncryptedBalance must also exist (and vice versa).
* - COA <= OutstandingAmount:
* Confidential outstanding balance cannot exceed total outstanding.
* - Verifies sfConfidentialBalanceVersion is changed whenever sfConfidentialBalanceSpending is
* modified on an MPToken.
*/
class ValidConfidentialMPToken
{
struct Changes
{
std::int64_t mptAmountDelta = 0;
std::int64_t coaDelta = 0;
std::int64_t outstandingDelta = 0;
SLE::const_pointer issuance;
bool deletedWithEncrypted = false;
bool badConsistency = false;
bool badCOA = false;
bool requiresPrivacyFlag = false;
bool badVersion = false;
};
std::map<uint192, Changes> changes_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
} // namespace xrpl

View File

@@ -0,0 +1,29 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class CredentialAccept : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CredentialAccept(ApplyContext& ctx) : Transactor(ctx)
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -0,0 +1,29 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class CredentialCreate : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CredentialCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -0,0 +1,29 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class CredentialDelete : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CredentialDelete(ApplyContext& ctx) : Transactor(ctx)
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -1,77 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class CredentialCreate : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CredentialCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
class CredentialDelete : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CredentialDelete(ApplyContext& ctx) : Transactor(ctx)
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
class CredentialAccept : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CredentialAccept(ApplyContext& ctx) : Transactor(ctx)
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -4,24 +4,6 @@
namespace xrpl {
class DIDSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit DIDSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
class DIDDelete : public Transactor
{
public:

View File

@@ -0,0 +1,23 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class DIDSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit DIDSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -1,80 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class EscrowCreate : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit EscrowCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
class EscrowFinish : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit EscrowFinish(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static NotTEC
preflightSigValidated(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
class EscrowCancel : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit EscrowCancel(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -0,0 +1,26 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class EscrowCancel : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit EscrowCancel(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -0,0 +1,29 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class EscrowCreate : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit EscrowCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -0,0 +1,35 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class EscrowFinish : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit EscrowFinish(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static NotTEC
preflightSigValidated(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -1,83 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class PayChanCreate : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit PayChanCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
using PaymentChannelCreate = PayChanCreate;
//------------------------------------------------------------------------------
class PayChanFund : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit PayChanFund(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
TER
doApply() override;
};
using PaymentChannelFund = PayChanFund;
//------------------------------------------------------------------------------
class PayChanClaim : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit PayChanClaim(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
using PaymentChannelClaim = PayChanClaim;
} // namespace xrpl

View File

@@ -0,0 +1,34 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class PayChanClaim : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit PayChanClaim(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
using PaymentChannelClaim = PayChanClaim;
} // namespace xrpl

View File

@@ -0,0 +1,31 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class PayChanCreate : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit PayChanCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
using PaymentChannelCreate = PayChanCreate;
} // namespace xrpl

View File

@@ -0,0 +1,28 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class PayChanFund : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit PayChanFund(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
TER
doApply() override;
};
using PaymentChannelFund = PayChanFund;
} // namespace xrpl

View File

@@ -49,11 +49,6 @@ public:
ttLOAN_DELETE,
ttLOAN_MANAGE,
ttLOAN_PAY,
ttCONFIDENTIAL_MPT_SEND,
ttCONFIDENTIAL_MPT_CONVERT,
ttCONFIDENTIAL_MPT_CONVERT_BACK,
ttCONFIDENTIAL_MPT_MERGE_INBOX,
ttCONFIDENTIAL_MPT_CLAWBACK,
});
};

View File

@@ -1,42 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
/**
* @brief Allows an MPT issuer to clawback confidential balances from a holder.
*
* This transaction enables the issuer of an MPToken Issuance (with clawback
* enabled) to reclaim confidential tokens from a holder's account. Unlike
* regular clawback, the issuer cannot see the holder's balance directly.
* Instead, the issuer must provide a zero-knowledge proof that demonstrates
* they know the exact encrypted balance amount.
*
* @par Cryptographic Operations:
* - **Equality Proof Verification**: Verifies that the issuer's revealed
* amount matches the holder's encrypted balance using the issuer's
* ElGamal private key.
*
* @see ConfidentialMPTSend, ConfidentialMPTConvert
*/
class ConfidentialMPTClawback : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialMPTClawback(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -1,44 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
/**
* @brief Converts public (plaintext) MPT balance to confidential (encrypted)
* balance.
*
* This transaction allows a token holder to convert their publicly visible
* MPToken balance into an encrypted confidential balance. Once converted,
* the balance can only be spent using ConfidentialMPTSend transactions and
* remains hidden from public view on the ledger.
*
* @par Cryptographic Operations:
* - **Schnorr Proof Verification**: When registering a new ElGamal public key,
* verifies proof of knowledge of the corresponding private key.
* - **Revealed Amount Verification**: Verifies that the provided encrypted
* amounts (for holder, issuer, and optionally auditor) all encrypt the
* same plaintext amount using the provided blinding factor.
*
* @see ConfidentialMPTConvertBack, ConfidentialMPTSend
*/
class ConfidentialMPTConvert : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialMPTConvert(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -1,45 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
/**
* @brief Converts confidential (encrypted) MPT balance back to public
* (plaintext) balance.
*
* This transaction allows a token holder to convert their encrypted
* confidential balance back into a publicly visible MPToken balance. The
* holder must prove they have sufficient confidential balance without
* revealing the actual balance amount.
*
* @par Cryptographic Operations:
* - **Revealed Amount Verification**: Verifies that the provided encrypted
* amounts correctly encrypt the conversion amount.
* - **Pedersen Linkage Proof**: Verifies that the provided balance commitment
* correctly links to the holder's encrypted spending balance.
* - **Bulletproof Range Proof**: Verifies that the remaining balance (after
* conversion) is non-negative, ensuring the holder has sufficient funds.
*
* @see ConfidentialMPTConvert, ConfidentialMPTSend
*/
class ConfidentialMPTConvertBack : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialMPTConvertBack(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -1,46 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
/**
* @brief Merges the confidential inbox balance into the spending balance.
*
* In the confidential transfer system, incoming funds are deposited into an
* "inbox" balance that the recipient cannot immediately spend. This prevents
* front-running attacks where an attacker could invalidate a pending
* transaction by sending funds to the sender. This transaction merges the
* inbox into the spending balance, making those funds available for spending.
*
* @par Cryptographic Operations:
* - **Homomorphic Addition**: Adds the encrypted inbox balance to the
* encrypted spending balance using ElGamal homomorphic properties.
* - **Zero Encryption**: Resets the inbox to an encryption of zero.
*
* @note This transaction requires no zero-knowledge proofs because it only
* combines encrypted values that the holder already owns. The
* homomorphic properties of ElGamal encryption ensure correctness.
*
* @see ConfidentialMPTSend, ConfidentialMPTConvert
*/
class ConfidentialMPTMergeInbox : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialMPTMergeInbox(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -1,52 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
/**
* @brief Transfers confidential MPT tokens between holders privately.
*
* This transaction enables private token transfers where the transfer amount
* is hidden from public view. Both sender and recipient must have initialized
* confidential balances. The transaction provides encrypted amounts for all
* parties (sender, destination, issuer, and optionally auditor) along with
* zero-knowledge proofs that verify correctness without revealing the amount.
*
* @par Cryptographic Operations:
* - **Multi-Ciphertext Equality Proof**: Verifies that all encrypted amounts
* (sender, destination, issuer, auditor) encrypt the same plaintext value.
* - **Amount Pedersen Linkage Proof**: Verifies that the amount commitment
* correctly links to the sender's encrypted amount.
* - **Balance Pedersen Linkage Proof**: Verifies that the balance commitment
* correctly links to the sender's encrypted spending balance.
* - **Bulletproof Range Proof**: Verifies remaining balance and
* transfer amount are non-negative.
*
* @note Funds are deposited into the destination's inbox, not spending
* balance. The recipient must call ConfidentialMPTMergeInbox to make
* received funds spendable.
*
* @see ConfidentialMPTMergeInbox, ConfidentialMPTConvert,
* ConfidentialMPTConvertBack
*/
class ConfidentialMPTSend : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialMPTSend(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -168,7 +168,7 @@ decode(void* dest, char const* src, std::size_t len)
break;
++in;
c4[i] = v;
if (++i == 4)
if (++i; i == 4)
{
c3[0] = (c4[0] << 2) + ((c4[1] & 0x30) >> 4);
c3[1] = ((c4[1] & 0xf) << 4) + ((c4[2] & 0x3c) >> 2);

View File

@@ -62,7 +62,8 @@ namespace beast::detail {
inline void
setCurrentThreadNameImpl(std::string_view name)
{
pthread_setname_np(name.data());
// The string is assumed to be null terminated
pthread_setname_np(name.data()); // NOLINT(bugprone-suspicious-stringview-data-usage)
}
} // namespace beast::detail
@@ -85,7 +86,7 @@ setCurrentThreadNameImpl(std::string_view name)
sizeof(boundedName),
"%.*s",
static_cast<int>(maxThreadNameLength),
name.data());
name.data()); // NOLINT(bugprone-suspicious-stringview-data-usage)
pthread_setname_np(pthread_self(), boundedName);

View File

@@ -729,21 +729,18 @@ Reader::decodeUnicodeCodePoint(Token& token, Location& current, Location end, un
unsigned int surrogatePair;
if (*(current++) == '\\' && *(current++) == 'u')
{
if (decodeUnicodeEscapeSequence(token, current, end, surrogatePair))
{
unicode = 0x10000 + ((unicode & 0x3FF) << 10) + (surrogatePair & 0x3FF);
}
else
return false;
}
else
if (*current != '\\' || *(current + 1) != 'u')
return addError(
"expecting another \\u token to begin the second half of a "
"unicode surrogate pair",
"expecting another \\u token to begin the second half of a unicode surrogate pair",
token,
current);
current += 2; // skip two characters checked above
if (!decodeUnicodeEscapeSequence(token, current, end, surrogatePair))
return false;
unicode = 0x10000 + ((unicode & 0x3FF) << 10) + (surrogatePair & 0x3FF);
}
return true;

View File

@@ -319,7 +319,7 @@ StyledWriter::writeValue(Value const& value)
document_ += " : ";
writeValue(childValue);
if (++it == members.end())
if (++it; it == members.end())
break;
document_ += ",";

View File

@@ -74,8 +74,10 @@ BookDirs::const_iterator::operator++()
XRPL_ASSERT(index_ != zero, "xrpl::BookDirs::const_iterator::operator++ : nonzero index");
if (!cdirNext(*view_, cur_key_, sle_, entry_, index_))
{
if (index_ != 0 ||
(cur_key_ = view_->succ(++cur_key_, next_quality_).value_or(zero)) == zero)
if (index_ == 0)
cur_key_ = view_->succ(++cur_key_, next_quality_).value_or(zero);
if (index_ != 0 || cur_key_ == zero)
{
cur_key_ = key_;
entry_ = 0;
@@ -84,9 +86,7 @@ BookDirs::const_iterator::operator++()
else if (!cdirFirst(*view_, cur_key_, sle_, entry_, index_))
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::BookDirs::const_iterator::operator++ : directory is "
"empty");
UNREACHABLE("xrpl::BookDirs::const_iterator::operator++ : directory is empty");
// LCOV_EXCL_STOP
}
}

View File

@@ -524,8 +524,7 @@ accountHolds(
// Only if auth check is needed, as it needs to do an additional read
// operation. Note featureSingleAssetVault will affect error codes.
if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED &&
(view.rules().enabled(featureSingleAssetVault) ||
view.rules().enabled(featureConfidentialTransfer)))
view.rules().enabled(featureSingleAssetVault))
{
if (auto const err = requireAuth(view, mptIssue, account, AuthType::StrongAuth);
!isTesSuccess(err))

View File

@@ -1,810 +0,0 @@
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Protocol.h>
#include <boost/endian/conversion.hpp>
#include <openssl/rand.h>
#include <openssl/sha.h>
namespace xrpl {
static constexpr std::uint32_t defaultVersion = 0;
void
addCommonZKPFields(
Serializer& s,
std::uint16_t txType,
AccountID const& account,
uint192 const& issuanceID,
std::uint32_t sequence)
{
// TxCommonHash = hash(TxType || Account || IssuanceID || SequenceOrTicket)
s.add16(txType);
s.addBitString(account);
s.addBitString(issuanceID);
s.add32(sequence);
}
uint256
getSendContextHash(
AccountID const& account,
uint192 const& issuanceID,
std::uint32_t sequence,
AccountID const& destination,
std::uint32_t version)
{
Serializer s;
addCommonZKPFields(s, ttCONFIDENTIAL_MPT_SEND, account, issuanceID, sequence);
// TxSpecific = identity || freshness
s.addBitString(destination);
s.addInteger(version);
return s.getSHA512Half();
}
uint256
getClawbackContextHash(
AccountID const& account,
uint192 const& issuanceID,
std::uint32_t sequence,
AccountID const& holder)
{
Serializer s;
addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CLAWBACK, account, issuanceID, sequence);
// TxSpecific = identity || freshness
s.addBitString(holder);
s.addInteger(defaultVersion);
return s.getSHA512Half();
}
uint256
getConvertContextHash(AccountID const& account, uint192 const& issuanceID, std::uint32_t sequence)
{
Serializer s;
addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CONVERT, account, issuanceID, sequence);
// TxSpecific = identity || freshness
s.addBitString(account);
s.addInteger(defaultVersion);
return s.getSHA512Half();
}
uint256
getConvertBackContextHash(
AccountID const& account,
uint192 const& issuanceID,
std::uint32_t sequence,
std::uint32_t version)
{
Serializer s;
addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CONVERT_BACK, account, issuanceID, sequence);
// TxSpecific = identity || freshness
s.addBitString(account);
s.addInteger(version);
return s.getSHA512Half();
}
std::optional<EcPair>
makeEcPair(Slice const& buffer)
{
if (buffer.length() != 2 * ecGamalEncryptedLength)
return std::nullopt; // LCOV_EXCL_LINE
auto parsePubKey = [](Slice const& slice, secp256k1_pubkey& out) {
return secp256k1_ec_pubkey_parse(
secp256k1Context(),
&out,
reinterpret_cast<unsigned char const*>(slice.data()),
slice.length());
};
Slice s1{buffer.data(), ecGamalEncryptedLength};
Slice s2{buffer.data() + ecGamalEncryptedLength, ecGamalEncryptedLength};
EcPair pair;
if (parsePubKey(s1, pair.c1) != 1 || parsePubKey(s2, pair.c2) != 1)
return std::nullopt;
return pair;
}
std::optional<Buffer>
serializeEcPair(EcPair const& pair)
{
auto serializePubKey = [](secp256k1_pubkey const& pub, unsigned char* out) {
size_t outLen = ecGamalEncryptedLength; // 33 bytes
int const ret = secp256k1_ec_pubkey_serialize(
secp256k1Context(), out, &outLen, &pub, SECP256K1_EC_COMPRESSED);
return ret == 1 && outLen == ecGamalEncryptedLength;
};
Buffer buffer(ecGamalEncryptedTotalLength);
unsigned char* ptr = buffer.data();
bool const res1 = serializePubKey(pair.c1, ptr);
bool const res2 = serializePubKey(pair.c2, ptr + ecGamalEncryptedLength);
if (!res1 || !res2)
return std::nullopt;
return buffer;
}
bool
isValidCiphertext(Slice const& buffer)
{
return makeEcPair(buffer).has_value();
}
bool
isValidCompressedECPoint(Slice const& buffer)
{
if (buffer.size() != compressedECPointLength)
return false;
// Compressed EC points must start with 0x02 or 0x03
if (buffer[0] != ecCompressedPrefixEvenY && buffer[0] != ecCompressedPrefixOddY)
return false;
secp256k1_pubkey point;
return secp256k1_ec_pubkey_parse(secp256k1Context(), &point, buffer.data(), buffer.size()) == 1;
}
std::optional<Buffer>
homomorphicAdd(Slice const& a, Slice const& b)
{
if (a.length() != ecGamalEncryptedTotalLength || b.length() != ecGamalEncryptedTotalLength)
return std::nullopt;
auto const pairA = makeEcPair(a);
auto const pairB = makeEcPair(b);
if (!pairA || !pairB)
return std::nullopt;
EcPair sum;
if (auto res = secp256k1_elgamal_add(
secp256k1Context(), &sum.c1, &sum.c2, &pairA->c1, &pairA->c2, &pairB->c1, &pairB->c2);
res != 1)
{
return std::nullopt;
}
return serializeEcPair(sum);
}
std::optional<Buffer>
homomorphicSubtract(Slice const& a, Slice const& b)
{
if (a.length() != ecGamalEncryptedTotalLength || b.length() != ecGamalEncryptedTotalLength)
return std::nullopt;
auto const pairA = makeEcPair(a);
auto const pairB = makeEcPair(b);
if (!pairA || !pairB)
return std::nullopt;
EcPair diff;
if (auto res = secp256k1_elgamal_subtract(
secp256k1Context(), &diff.c1, &diff.c2, &pairA->c1, &pairA->c2, &pairB->c1, &pairB->c2);
res != 1)
{
return std::nullopt;
}
return serializeEcPair(diff);
}
Buffer
generateBlindingFactor()
{
unsigned char blindingFactor[ecBlindingFactorLength];
// todo: might need to be updated using another RNG
if (RAND_bytes(blindingFactor, ecBlindingFactorLength) != 1)
Throw<std::runtime_error>("Failed to generate random number");
return Buffer(blindingFactor, ecBlindingFactorLength);
}
std::optional<Buffer>
encryptAmount(uint64_t const amt, Slice const& pubKeySlice, Slice const& blindingFactor)
{
if (blindingFactor.size() != ecBlindingFactorLength)
return std::nullopt;
if (pubKeySlice.size() != ecPubKeyLength)
return std::nullopt;
EcPair pair;
secp256k1_pubkey pubKey;
if (auto res = secp256k1_ec_pubkey_parse(
secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength);
res != 1)
{
return std::nullopt;
}
if (auto res = secp256k1_elgamal_encrypt(
secp256k1Context(), &pair.c1, &pair.c2, &pubKey, amt, blindingFactor.data());
res != 1)
{
return std::nullopt;
}
return serializeEcPair(pair);
}
std::optional<Buffer>
encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId)
{
if (pubKeySlice.size() != ecPubKeyLength)
return std::nullopt; // LCOV_EXCL_LINE
EcPair pair;
secp256k1_pubkey pubKey;
if (auto res = secp256k1_ec_pubkey_parse(
secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength);
res != 1)
{
return std::nullopt; // LCOV_EXCL_LINE
}
if (auto res = generate_canonical_encrypted_zero(
secp256k1Context(), &pair.c1, &pair.c2, &pubKey, account.data(), mptId.data());
res != 1)
{
return std::nullopt; // LCOV_EXCL_LINE
}
return serializeEcPair(pair);
}
TER
verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash)
{
if (proofSlice.size() != ecSchnorrProofLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (pubKeySlice.size() != ecPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pubKey;
if (auto res = secp256k1_ec_pubkey_parse(
secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
if (auto res = secp256k1_mpt_pok_sk_verify(
secp256k1Context(), proofSlice.data(), &pubKey, contextHash.data());
res != 1)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
verifyElGamalEncryption(
uint64_t const amount,
Slice const& blindingFactor,
Slice const& pubKeySlice,
Slice const& ciphertext)
{
if (ciphertext.size() != ecGamalEncryptedTotalLength ||
blindingFactor.size() != ecBlindingFactorLength || pubKeySlice.size() != ecPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pubKey;
if (auto res = secp256k1_ec_pubkey_parse(
secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
auto const pair = makeEcPair(ciphertext);
if (!pair)
return tecINTERNAL; // LCOV_EXCL_LINE
if (auto res = secp256k1_elgamal_verify_encryption(
secp256k1Context(), &pair->c1, &pair->c2, &pubKey, amount, blindingFactor.data());
res != 1)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
verifyRevealedAmount(
uint64_t const amount,
Slice const& blindingFactor,
ConfidentialRecipient const& holder,
ConfidentialRecipient const& issuer,
std::optional<ConfidentialRecipient> const& auditor)
{
if (auto const res = verifyElGamalEncryption(
amount, blindingFactor, holder.publicKey, holder.encryptedAmount);
!isTesSuccess(res))
{
return res;
}
if (auto const res = verifyElGamalEncryption(
amount, blindingFactor, issuer.publicKey, issuer.encryptedAmount);
!isTesSuccess(res))
{
return res;
}
if (auditor)
{
if (auto const res = verifyElGamalEncryption(
amount, blindingFactor, auditor->publicKey, auditor->encryptedAmount);
!isTesSuccess(res))
{
return res;
}
}
return tesSUCCESS;
}
TER
verifyMultiCiphertextEqualityProof(
Slice const& proof,
std::vector<ConfidentialRecipient> const& recipients,
std::size_t const nRecipients,
uint256 const& contextHash)
{
if (recipients.size() != nRecipients)
return tecINTERNAL; // LCOV_EXCL_LINE
if (proof.size() != secp256k1_mpt_proof_equality_shared_r_size(nRecipients))
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ctx = secp256k1Context();
secp256k1_pubkey c1;
std::vector<secp256k1_pubkey> c2_vec(nRecipients);
std::vector<secp256k1_pubkey> pk_vec(nRecipients);
for (size_t i = 0; i < nRecipients; ++i)
{
auto const& recipient = recipients[i];
if (recipient.encryptedAmount.size() != ecGamalEncryptedTotalLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (recipient.publicKey.size() != ecPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
// Parse Shared C1 from the first recipient only
if (i == 0)
{
if (auto res = secp256k1_ec_pubkey_parse(
ctx, &c1, recipient.encryptedAmount.data(), ecGamalEncryptedLength);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
}
else
{
// All C1 bytes must be the same
if (std::memcmp(
recipient.encryptedAmount.data(),
recipients[0].encryptedAmount.data(),
ecGamalEncryptedLength) != 0)
{
return tecBAD_PROOF;
}
}
if (auto res = secp256k1_ec_pubkey_parse(
ctx,
&c2_vec[i],
recipient.encryptedAmount.data() + ecGamalEncryptedLength,
ecGamalEncryptedLength);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
if (auto res = secp256k1_ec_pubkey_parse(
ctx, &pk_vec[i], recipient.publicKey.data(), ecPubKeyLength);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
}
if (auto res = secp256k1_mpt_verify_equality_shared_r(
ctx, proof.data(), nRecipients, &c1, c2_vec.data(), pk_vec.data(), contextHash.data());
res != 1)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
verifyClawbackEqualityProof(
uint64_t const amount,
Slice const& proof,
Slice const& pubKeySlice,
Slice const& ciphertext,
uint256 const& contextHash)
{
if (ciphertext.size() != ecGamalEncryptedTotalLength || pubKeySlice.size() != ecPubKeyLength ||
proof.size() != ecEqualityProofLength)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const pair = makeEcPair(ciphertext);
if (!pair)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pubKey;
if (auto res = secp256k1_ec_pubkey_parse(
secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
// Note: c2, c1 order - the proof is generated with c2 first (the encrypted
// message component) because the equality proof structure expects the
// message-containing term before the blinding term.
if (auto res = secp256k1_equality_plaintext_verify(
secp256k1Context(),
proof.data(),
&pubKey,
&pair->c2,
&pair->c1,
amount,
contextHash.data());
res != 1)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
NotTEC
checkEncryptedAmountFormat(STObject const& object)
{
// Current usage of this function is only for ConfidentialMPTConvert and
// ConfidentialMPTConvertBack transactions, which already enforce that these fields
// are present.
if (!object.isFieldPresent(sfHolderEncryptedAmount) ||
!object.isFieldPresent(sfIssuerEncryptedAmount))
return temMALFORMED; // LCOV_EXCL_LINE
if (object[sfHolderEncryptedAmount].length() != ecGamalEncryptedTotalLength ||
object[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
bool const hasAuditor = object.isFieldPresent(sfAuditorEncryptedAmount);
if (hasAuditor && object[sfAuditorEncryptedAmount].length() != ecGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
if (!isValidCiphertext(object[sfHolderEncryptedAmount]) ||
!isValidCiphertext(object[sfIssuerEncryptedAmount]))
return temBAD_CIPHERTEXT;
if (hasAuditor && !isValidCiphertext(object[sfAuditorEncryptedAmount]))
return temBAD_CIPHERTEXT;
return tesSUCCESS;
}
TER
verifyAmountPcmLinkage(
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,
Slice const& pcmSlice,
uint256 const& contextHash)
{
if (proof.length() != ecPedersenProofLength)
return tecINTERNAL;
auto const pair = makeEcPair(encAmt);
if (!pair)
return tecINTERNAL; // LCOV_EXCL_LINE
if (pubKeySlice.size() != ecPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (pcmSlice.size() != ecPedersenCommitmentLength)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pubKey;
if (auto res = secp256k1_ec_pubkey_parse(
secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
secp256k1_pubkey pcm;
if (auto res = secp256k1_ec_pubkey_parse(
secp256k1Context(), &pcm, pcmSlice.data(), ecPedersenCommitmentLength);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
if (auto res = secp256k1_elgamal_pedersen_link_verify(
secp256k1Context(),
proof.data(),
&pair->c1,
&pair->c2,
&pubKey,
&pcm,
contextHash.data());
res != 1)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
verifyBalancePcmLinkage(
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,
Slice const& pcmSlice,
uint256 const& contextHash)
{
if (proof.length() != ecPedersenProofLength)
return tecINTERNAL;
auto const pair = makeEcPair(encAmt);
if (!pair)
return tecINTERNAL; // LCOV_EXCL_LINE
if (pubKeySlice.size() != ecPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (pcmSlice.size() != ecPedersenCommitmentLength)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pubKey;
if (auto res = secp256k1_ec_pubkey_parse(
secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
secp256k1_pubkey pcm;
if (auto res = secp256k1_ec_pubkey_parse(
secp256k1Context(), &pcm, pcmSlice.data(), ecPedersenCommitmentLength);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
// Note: c2, c1 order - the linkage proof expects the message-containing
// component (c2 = m*G + r*Pk) before the blinding component (c1 = r*G).
if (auto res = secp256k1_elgamal_pedersen_link_verify(
secp256k1Context(),
proof.data(),
&pubKey,
&pair->c2,
&pair->c1,
&pcm,
contextHash.data());
res != 1)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
verifyAggregatedBulletproof(
Slice const& proof,
std::vector<Slice> const& compressedCommitments,
uint256 const& contextHash)
{
// 1. Validate input lengths
// This function could support any power-of-2 m, but current usage only requires m=1 or m=2
std::size_t const m = compressedCommitments.size();
if (m != 1 && m != 2)
return tecINTERNAL; // LCOV_EXCL_LINE
std::size_t const expectedProofLen =
(m == 1) ? ecSingleBulletproofLength : ecDoubleBulletproofLength;
if (proof.size() != expectedProofLen)
return tecINTERNAL; // LCOV_EXCL_LINE
// 2. Prepare Pedersen Commitments, parse from compressed format
auto const ctx = secp256k1Context();
std::vector<secp256k1_pubkey> commitments(m);
for (size_t i = 0; i < m; ++i)
{
// Sanity check length
if (compressedCommitments[i].size() != ecPedersenCommitmentLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (auto res = secp256k1_ec_pubkey_parse(
ctx, &commitments[i], compressedCommitments[i].data(), ecPedersenCommitmentLength);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
}
// 3. Prepare Generator Vectors (G_vec, H_vec)
// The range proof requires vectors of size 64 * m
std::size_t const n = 64 * m;
std::vector<secp256k1_pubkey> G_vec(n);
std::vector<secp256k1_pubkey> H_vec(n);
// Retrieve deterministic generators "G" and "H"
if (auto res =
secp256k1_mpt_get_generator_vector(ctx, G_vec.data(), n, (unsigned char const*)"G", 1);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
if (auto res =
secp256k1_mpt_get_generator_vector(ctx, H_vec.data(), n, (unsigned char const*)"H", 1);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
// 4. Prepare Base Generator (pk_base / H)
secp256k1_pubkey pk_base;
if (auto res = secp256k1_mpt_get_h_generator(ctx, &pk_base); res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
// 5. Verify the Proof
if (auto res = secp256k1_bulletproof_verify_agg(
ctx,
G_vec.data(),
H_vec.data(),
reinterpret_cast<unsigned char const*>(proof.data()),
proof.size(),
commitments.data(),
m,
&pk_base,
contextHash.data());
res != 1)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment, Buffer& out)
{
if (balanceCommitment.size() != ecPedersenCommitmentLength ||
amountCommitment.size() != ecPedersenCommitmentLength)
return tecINTERNAL;
auto const ctx = secp256k1Context();
secp256k1_pubkey pcBalance;
if (auto res = secp256k1_ec_pubkey_parse(
ctx, &pcBalance, balanceCommitment.data(), ecPedersenCommitmentLength);
res != 1)
{
return tecINTERNAL;
}
secp256k1_pubkey pcAmount;
if (auto res = secp256k1_ec_pubkey_parse(
ctx, &pcAmount, amountCommitment.data(), ecPedersenCommitmentLength);
res != 1)
{
return tecINTERNAL;
}
// Negate PC_amount point to get -PC_amount
if (auto res = secp256k1_ec_pubkey_negate(ctx, &pcAmount); res != 1)
{
return tecINTERNAL;
}
// Compute pcRem = pcBalance + (-pcAmount)
secp256k1_pubkey const* summands[2] = {&pcBalance, &pcAmount};
secp256k1_pubkey pcRem;
if (auto res = secp256k1_ec_pubkey_combine(ctx, &pcRem, summands, 2); res != 1)
{
return tecINTERNAL;
}
// Serialize result to compressed format
out.alloc(ecPedersenCommitmentLength);
size_t outLen = ecPedersenCommitmentLength;
if (auto res = secp256k1_ec_pubkey_serialize(
ctx, out.data(), &outLen, &pcRem, SECP256K1_EC_COMPRESSED);
res != 1)
{
return tecINTERNAL;
}
return tesSUCCESS;
}
TER
computeConvertBackRemainder(Slice const& commitment, uint64_t amount, Buffer& out)
{
if (commitment.size() != ecPedersenCommitmentLength || amount == 0)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ctx = secp256k1Context();
// Parse commitment from compressed format
secp256k1_pubkey pcBalance;
if (auto res = secp256k1_ec_pubkey_parse(
ctx, &pcBalance, commitment.data(), ecPedersenCommitmentLength);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
// Convert amount to 32-byte big-endian scalar
unsigned char mScalar[32] = {0};
uint64_t amountBigEndian = boost::endian::native_to_big(amount);
std::memcpy(&mScalar[24], &amountBigEndian, sizeof(amountBigEndian));
// Compute mG = amount * G
secp256k1_pubkey mG;
if (auto res = secp256k1_ec_pubkey_create(ctx, &mG, mScalar); res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
// Negate mG to get -mG
if (auto res = secp256k1_ec_pubkey_negate(ctx, &mG); res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
// Compute pcRem = pcBalance + (-mG)
secp256k1_pubkey const* summands[2] = {&pcBalance, &mG};
secp256k1_pubkey pcRem;
if (auto res = secp256k1_ec_pubkey_combine(ctx, &pcRem, summands, 2); res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
// Serialize result to compressed format
out.alloc(ecPedersenCommitmentLength);
size_t outLen = ecPedersenCommitmentLength;
if (auto res = secp256k1_ec_pubkey_serialize(
ctx, out.data(), &outLen, &pcRem, SECP256K1_EC_COMPRESSED);
res != 1 || outLen != ecPedersenCommitmentLength)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -86,7 +86,7 @@ Permission::getPermissionName(std::uint32_t const value) const
{
auto const permissionValue = static_cast<GranularPermissionType>(value);
if (auto const granular = getGranularName(permissionValue))
return *granular;
return granular;
// not a granular permission, check if it maps to a transaction type
auto const txType = permissionToTxType(value);

View File

@@ -3,7 +3,6 @@
#include <xrpl/basics/contract.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/detail/secp256k1.h>
@@ -207,7 +206,7 @@ publicKeyType(Slice const& slice)
if (slice[0] == 0xED)
return KeyType::ed25519;
if (slice[0] == ecCompressedPrefixEvenY || slice[0] == ecCompressedPrefixOddY)
if (slice[0] == 0x02 || slice[0] == 0x03)
return KeyType::secp256k1;
}

View File

@@ -23,6 +23,9 @@ STBase::STBase(SField const& n) : fName(&n)
STBase&
STBase::operator=(STBase const& t)
{
if (this == &t)
return *this;
if (!fName->isUseful())
fName = t.fName;
return *this;

View File

@@ -106,7 +106,6 @@ transResults()
MAKE_ERROR(tecLIMIT_EXCEEDED, "Limit exceeded."),
MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."),
MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."),
MAKE_ERROR(tecBAD_PROOF, "Proof cannot be verified"),
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
@@ -199,7 +198,6 @@ transResults()
MAKE_ERROR(temARRAY_TOO_LARGE, "Malformed: Array is too large."),
MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."),
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."),
MAKE_ERROR(terRETRY, "Retry transaction."),
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),

View File

@@ -39,6 +39,9 @@ Consumer::~Consumer()
Consumer&
Consumer::operator=(Consumer const& other)
{
if (this == &other)
return *this;
// remove old ref
if (m_logic && m_entry)
m_logic->release(*m_entry);

View File

@@ -488,8 +488,9 @@ AccountRootsDeletedClean::finalize(
return false;
}
// Simple types
for (auto const& [keyletfunc, _, __] : directAccountKeylets)
for (auto const& [keyletfunc, _1, _2] : directAccountKeylets)
{
// TODO: use '_' for both unused variables above once we are in C++26
if (objectExists(std::invoke(keyletfunc, accountID)) && enforce)
return false;
}

View File

@@ -189,208 +189,4 @@ ValidMPTIssuance::finalize(
mptokensDeleted_ == 0;
}
void
ValidConfidentialMPToken::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
// Helper to get MPToken Issuance ID safely
auto const getMptID = [](std::shared_ptr<SLE const> const& sle) -> uint192 {
if (!sle)
return beast::zero;
if (sle->getType() == ltMPTOKEN)
return sle->getFieldH192(sfMPTokenIssuanceID);
if (sle->getType() == ltMPTOKEN_ISSUANCE)
return makeMptID(sle->getFieldU32(sfSequence), sle->getAccountID(sfIssuer));
return beast::zero;
};
if (before && before->getType() == ltMPTOKEN)
{
uint192 const id = getMptID(before);
changes_[id].mptAmountDelta -= before->getFieldU64(sfMPTAmount);
// Cannot delete MPToken with non-zero confidential state or non-zero public amount
if (isDelete)
{
bool const hasPublicBalance = before->getFieldU64(sfMPTAmount) > 0;
bool const hasEncryptedFields = before->isFieldPresent(sfConfidentialBalanceSpending) ||
before->isFieldPresent(sfConfidentialBalanceInbox) ||
before->isFieldPresent(sfIssuerEncryptedBalance);
if (hasPublicBalance || hasEncryptedFields)
changes_[id].deletedWithEncrypted = true;
}
}
if (after && after->getType() == ltMPTOKEN)
{
uint192 const id = getMptID(after);
changes_[id].mptAmountDelta += after->getFieldU64(sfMPTAmount);
// Encrypted field existence consistency
bool const hasIssuerBalance = after->isFieldPresent(sfIssuerEncryptedBalance);
bool const hasHolderInbox = after->isFieldPresent(sfConfidentialBalanceInbox);
bool const hasHolderSpending = after->isFieldPresent(sfConfidentialBalanceSpending);
bool const hasAnyHolder = hasHolderInbox || hasHolderSpending;
if (hasAnyHolder != hasIssuerBalance)
{
changes_[id].badConsistency = true;
}
// Privacy flag consistency
bool const hasEncrypted = hasAnyHolder || hasIssuerBalance;
if (hasEncrypted)
changes_[id].requiresPrivacyFlag = true;
}
if (before && before->getType() == ltMPTOKEN_ISSUANCE)
{
uint192 const id = getMptID(before);
if (before->isFieldPresent(sfConfidentialOutstandingAmount))
changes_[id].coaDelta -= before->getFieldU64(sfConfidentialOutstandingAmount);
changes_[id].outstandingDelta -= before->getFieldU64(sfOutstandingAmount);
}
if (after && after->getType() == ltMPTOKEN_ISSUANCE)
{
uint192 const id = getMptID(after);
auto& change = changes_[id];
bool const hasCOA = after->isFieldPresent(sfConfidentialOutstandingAmount);
std::uint64_t const coa = (*after)[~sfConfidentialOutstandingAmount].value_or(0);
std::uint64_t const oa = after->getFieldU64(sfOutstandingAmount);
if (hasCOA)
change.coaDelta += coa;
change.outstandingDelta += oa;
change.issuance = after;
// COA <= OutstandingAmount
if (coa > oa)
change.badCOA = true;
}
if (before && after && before->getType() == ltMPTOKEN && after->getType() == ltMPTOKEN)
{
uint192 const id = getMptID(after);
// sfConfidentialBalanceVersion must change when spending changes
auto const spendingBefore = (*before)[~sfConfidentialBalanceSpending];
auto const spendingAfter = (*after)[~sfConfidentialBalanceSpending];
auto const versionBefore = (*before)[~sfConfidentialBalanceVersion];
auto const versionAfter = (*after)[~sfConfidentialBalanceVersion];
if (spendingBefore.has_value() && spendingBefore != spendingAfter)
{
if (versionBefore == versionAfter)
{
changes_[id].badVersion = true;
}
}
}
}
bool
ValidConfidentialMPToken::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
if (result != tesSUCCESS)
return true;
for (auto const& [id, checks] : changes_)
{
// Find the MPTokenIssuance
auto const issuance = [&]() -> std::shared_ptr<SLE const> {
if (checks.issuance)
return checks.issuance;
return view.read(keylet::mptIssuance(id));
}();
// Skip all invariance checks if issuance doesn't exist because that means the MPT has been
// deleted
if (!issuance)
continue;
// Cannot delete MPToken with non-zero confidential state
if (checks.deletedWithEncrypted)
{
if ((*issuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
{
JLOG(j.fatal())
<< "Invariant failed: MPToken deleted with encrypted fields while COA > 0";
return false;
}
}
// Encrypted field existence consistency
if (checks.badConsistency)
{
JLOG(j.fatal()) << "Invariant failed: MPToken encrypted field "
"existence inconsistency";
return false;
}
// COA <= OutstandingAmount
if (checks.badCOA)
{
JLOG(j.fatal()) << "Invariant failed: Confidential outstanding amount "
"exceeds total outstanding amount";
return false;
}
// Privacy flag consistency
if (checks.requiresPrivacyFlag)
{
if (!issuance->isFlag(lsfMPTCanConfidentialAmount))
{
JLOG(j.fatal()) << "Invariant failed: MPToken has encrypted "
"fields but Issuance does not have "
"lsfMPTCanConfidentialAmount set";
return false;
}
}
// We only enforce this when Confidential Outstanding Amount changes (Convert, ConvertBack,
// ConfidentialClawback). This avoids falsely failing on Escrow or AMM operations that lock
// public tokens outside of ltMPTOKEN. Convert / ConvertBack:
// - COA and MPTAmount must have opposite deltas, which cancel each other out to zero.
// - OA remains unchanged.
// - Therefore, the net delta on both sides of the equation is zero.
//
// Clawback:
// - MPTAmount remains unchanged.
// - COA and OA must have identical deltas (mirrored on each side).
// - The equation remains balanced as both sides have equal offsets.
if (checks.coaDelta != 0)
{
if (checks.mptAmountDelta + checks.coaDelta != checks.outstandingDelta)
{
JLOG(j.fatal()) << "Invariant failed: Token conservation "
"violation for MPT "
<< to_string(id);
return false;
}
}
if (checks.badVersion)
{
JLOG(j.fatal())
<< "Invariant failed: MPToken sfConfidentialBalanceVersion not updated when "
"sfConfidentialBalanceSpending changed";
return false;
}
}
return true;
}
} // namespace xrpl

View File

@@ -1,5 +1,4 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/mulDiv.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
@@ -11,7 +10,7 @@
#include <xrpl/tx/transactors/account/DeleteAccount.h>
#include <xrpl/tx/transactors/account/SetSignerList.h>
#include <xrpl/tx/transactors/delegate/DelegateSet.h>
#include <xrpl/tx/transactors/did/DID.h>
#include <xrpl/tx/transactors/did/DIDDelete.h>
#include <xrpl/tx/transactors/nft/NFTokenUtils.h>
#include <xrpl/tx/transactors/oracle/DeleteOracle.h>
#include <xrpl/tx/transactors/payment/DepositPreauth.h>

View File

@@ -491,7 +491,7 @@ SetAccount::doApply()
if (messageKey.empty())
{
JLOG(j_.debug()) << "set message key";
JLOG(j_.debug()) << "clear message key";
sle->makeFieldAbsent(sfMessageKey);
}
else

View File

@@ -868,7 +868,13 @@ applyClaimAttestations(
XChainClaimAttestations curAtts{sleClaimID->getFieldArray(sfXChainClaimAttestations)};
auto const newAttResult = onNewAttestations(
curAtts, view, &atts[0], &atts[0] + atts.size(), quorum, signersList, j);
curAtts,
view,
&atts[0],
&atts[0] + atts.size(), // NOLINT(bugprone-pointer-arithmetic-on-polymorphic-object)
quorum,
signersList,
j);
// update the claim id
sleClaimID->setFieldArray(sfXChainClaimAttestations, curAtts.toSTArray());
@@ -1026,7 +1032,13 @@ applyCreateAccountAttestations(
}();
auto const newAttResult = onNewAttestations(
curAtts, view, &atts[0], &atts[0] + atts.size(), quorum, signersList, j);
curAtts,
view,
&atts[0],
&atts[0] + atts.size(), // NOLINT(bugprone-pointer-arithmetic-on-polymorphic-object)
quorum,
signersList,
j);
if (!createCID)
{

View File

@@ -0,0 +1,113 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/credentials/CredentialAccept.h>
#include <chrono>
namespace xrpl {
using namespace credentials;
std::uint32_t
CredentialAccept::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0;
}
NotTEC
CredentialAccept::preflight(PreflightContext const& ctx)
{
if (!ctx.tx[sfIssuer])
{
JLOG(ctx.j.trace()) << "Malformed transaction: Issuer field zeroed.";
return temINVALID_ACCOUNT_ID;
}
auto const credType = ctx.tx[sfCredentialType];
if (credType.empty() || (credType.size() > maxCredentialTypeLength))
{
JLOG(ctx.j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
}
return tesSUCCESS;
}
TER
CredentialAccept::preclaim(PreclaimContext const& ctx)
{
AccountID const subject = ctx.tx[sfAccount];
AccountID const issuer = ctx.tx[sfIssuer];
auto const credType(ctx.tx[sfCredentialType]);
if (!ctx.view.exists(keylet::account(issuer)))
{
JLOG(ctx.j.warn()) << "No issuer: " << to_string(issuer);
return tecNO_ISSUER;
}
auto const sleCred = ctx.view.read(keylet::credential(subject, issuer, credType));
if (!sleCred)
{
JLOG(ctx.j.warn()) << "No credential: " << to_string(subject) << ", " << to_string(issuer)
<< ", " << credType;
return tecNO_ENTRY;
}
if (sleCred->getFieldU32(sfFlags) & lsfAccepted)
{
JLOG(ctx.j.warn()) << "Credential already accepted: " << to_string(subject) << ", "
<< to_string(issuer) << ", " << credType;
return tecDUPLICATE;
}
return tesSUCCESS;
}
TER
CredentialAccept::doApply()
{
AccountID const issuer{ctx_.tx[sfIssuer]};
// Both exist as credential object exist itself (checked in preclaim)
auto const sleSubject = view().peek(keylet::account(account_));
auto const sleIssuer = view().peek(keylet::account(issuer));
if (!sleSubject || !sleIssuer)
return tefINTERNAL; // LCOV_EXCL_LINE
{
STAmount const reserve{
view().fees().accountReserve(sleSubject->getFieldU32(sfOwnerCount) + 1)};
if (mPriorBalance < reserve)
return tecINSUFFICIENT_RESERVE;
}
auto const credType(ctx_.tx[sfCredentialType]);
Keylet const credentialKey = keylet::credential(account_, issuer, credType);
auto const sleCred = view().peek(credentialKey); // Checked in preclaim()
if (checkExpired(sleCred, view().header().parentCloseTime))
{
JLOG(j_.trace()) << "Credential is expired: " << sleCred->getText();
// delete expired credentials even if the transaction failed
auto const err = credentials::deleteSLE(view(), sleCred, j_);
return isTesSuccess(err) ? tecEXPIRED : err;
}
sleCred->setFieldU32(sfFlags, lsfAccepted);
view().update(sleCred);
adjustOwnerCount(view(), sleIssuer, -1, j_);
adjustOwnerCount(view(), sleSubject, 1, j_);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,164 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/credentials/CredentialCreate.h>
#include <chrono>
namespace xrpl {
/*
Credentials
======
A verifiable credentials (VC
https://en.wikipedia.org/wiki/Verifiable_credentials), as defined by the W3C
specification (https://www.w3.org/TR/vc-data-model-2.0/), is a
secure and tamper-evident way to represent information about a subject, such
as an individual, organization, or even an IoT device. These credentials are
issued by a trusted entity and can be verified by third parties without
directly involving the issuer at all.
*/
using namespace credentials;
std::uint32_t
CredentialCreate::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0;
}
NotTEC
CredentialCreate::preflight(PreflightContext const& ctx)
{
auto const& tx = ctx.tx;
auto& j = ctx.j;
if (!tx[sfSubject])
{
JLOG(j.trace()) << "Malformed transaction: Invalid Subject";
return temMALFORMED;
}
auto const uri = tx[~sfURI];
if (uri && (uri->empty() || (uri->size() > maxCredentialURILength)))
{
JLOG(j.trace()) << "Malformed transaction: invalid size of URI.";
return temMALFORMED;
}
auto const credType = tx[sfCredentialType];
if (credType.empty() || (credType.size() > maxCredentialTypeLength))
{
JLOG(j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
}
return tesSUCCESS;
}
TER
CredentialCreate::preclaim(PreclaimContext const& ctx)
{
auto const credType(ctx.tx[sfCredentialType]);
auto const subject = ctx.tx[sfSubject];
if (!ctx.view.exists(keylet::account(subject)))
{
JLOG(ctx.j.trace()) << "Subject doesn't exist.";
return tecNO_TARGET;
}
if (ctx.view.exists(keylet::credential(subject, ctx.tx[sfAccount], credType)))
{
JLOG(ctx.j.trace()) << "Credential already exists.";
return tecDUPLICATE;
}
return tesSUCCESS;
}
TER
CredentialCreate::doApply()
{
auto const subject = ctx_.tx[sfSubject];
auto const credType(ctx_.tx[sfCredentialType]);
Keylet const credentialKey = keylet::credential(subject, account_, credType);
auto const sleCred = std::make_shared<SLE>(credentialKey);
if (!sleCred)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const optExp = ctx_.tx[~sfExpiration];
if (optExp)
{
std::uint32_t const closeTime =
ctx_.view().header().parentCloseTime.time_since_epoch().count();
if (closeTime > *optExp)
{
JLOG(j_.trace()) << "Malformed transaction: "
"Expiration time is in the past.";
return tecEXPIRED;
}
sleCred->setFieldU32(sfExpiration, ctx_.tx.getFieldU32(sfExpiration));
}
auto const sleIssuer = view().peek(keylet::account(account_));
if (!sleIssuer)
return tefINTERNAL; // LCOV_EXCL_LINE
{
STAmount const reserve{
view().fees().accountReserve(sleIssuer->getFieldU32(sfOwnerCount) + 1)};
if (mPriorBalance < reserve)
return tecINSUFFICIENT_RESERVE;
}
sleCred->setAccountID(sfSubject, subject);
sleCred->setAccountID(sfIssuer, account_);
sleCred->setFieldVL(sfCredentialType, credType);
if (ctx_.tx.isFieldPresent(sfURI))
sleCred->setFieldVL(sfURI, ctx_.tx.getFieldVL(sfURI));
{
auto const page =
view().dirInsert(keylet::ownerDir(account_), credentialKey, describeOwnerDir(account_));
JLOG(j_.trace()) << "Adding Credential to owner directory " << to_string(credentialKey.key)
<< ": " << (page ? "success" : "failure");
if (!page)
return tecDIR_FULL;
sleCred->setFieldU64(sfIssuerNode, *page);
adjustOwnerCount(view(), sleIssuer, 1, j_);
}
if (subject == account_)
{
sleCred->setFieldU32(sfFlags, lsfAccepted);
}
else
{
auto const page =
view().dirInsert(keylet::ownerDir(subject), credentialKey, describeOwnerDir(subject));
JLOG(j_.trace()) << "Adding Credential to owner directory " << to_string(credentialKey.key)
<< ": " << (page ? "success" : "failure");
if (!page)
return tecDIR_FULL;
sleCred->setFieldU64(sfSubjectNode, *page);
view().update(view().peek(keylet::account(subject)));
}
view().insert(sleCred);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,90 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/credentials/CredentialDelete.h>
#include <chrono>
namespace xrpl {
using namespace credentials;
std::uint32_t
CredentialDelete::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0;
}
NotTEC
CredentialDelete::preflight(PreflightContext const& ctx)
{
auto const subject = ctx.tx[~sfSubject];
auto const issuer = ctx.tx[~sfIssuer];
if (!subject && !issuer)
{
// Neither field is present, the transaction is malformed.
JLOG(ctx.j.trace()) << "Malformed transaction: "
"No Subject or Issuer fields.";
return temMALFORMED;
}
// Make sure that the passed account is valid.
if ((subject && subject->isZero()) || (issuer && issuer->isZero()))
{
JLOG(ctx.j.trace()) << "Malformed transaction: Subject or Issuer "
"field zeroed.";
return temINVALID_ACCOUNT_ID;
}
auto const credType = ctx.tx[sfCredentialType];
if (credType.empty() || (credType.size() > maxCredentialTypeLength))
{
JLOG(ctx.j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
}
return tesSUCCESS;
}
TER
CredentialDelete::preclaim(PreclaimContext const& ctx)
{
AccountID const account{ctx.tx[sfAccount]};
auto const subject = ctx.tx[~sfSubject].value_or(account);
auto const issuer = ctx.tx[~sfIssuer].value_or(account);
auto const credType(ctx.tx[sfCredentialType]);
if (!ctx.view.exists(keylet::credential(subject, issuer, credType)))
return tecNO_ENTRY;
return tesSUCCESS;
}
TER
CredentialDelete::doApply()
{
auto const subject = ctx_.tx[~sfSubject].value_or(account_);
auto const issuer = ctx_.tx[~sfIssuer].value_or(account_);
auto const credType(ctx_.tx[sfCredentialType]);
auto const sleCred = view().peek(keylet::credential(subject, issuer, credType));
if (!sleCred)
return tefINTERNAL; // LCOV_EXCL_LINE
if ((subject != account_) && (issuer != account_) &&
!checkExpired(sleCred, ctx_.view().header().parentCloseTime))
{
JLOG(j_.trace()) << "Can't delete non-expired credential.";
return tecNO_PERMISSION;
}
return deleteSLE(view(), sleCred, j_);
}
} // namespace xrpl

View File

@@ -1,341 +0,0 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/credentials/Credentials.h>
#include <chrono>
namespace xrpl {
/*
Credentials
======
A verifiable credentials (VC
https://en.wikipedia.org/wiki/Verifiable_credentials), as defined by the W3C
specification (https://www.w3.org/TR/vc-data-model-2.0/), is a
secure and tamper-evident way to represent information about a subject, such
as an individual, organization, or even an IoT device. These credentials are
issued by a trusted entity and can be verified by third parties without
directly involving the issuer at all.
*/
using namespace credentials;
// ------- CREATE --------------------------
std::uint32_t
CredentialCreate::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0;
}
NotTEC
CredentialCreate::preflight(PreflightContext const& ctx)
{
auto const& tx = ctx.tx;
auto& j = ctx.j;
if (!tx[sfSubject])
{
JLOG(j.trace()) << "Malformed transaction: Invalid Subject";
return temMALFORMED;
}
auto const uri = tx[~sfURI];
if (uri && (uri->empty() || (uri->size() > maxCredentialURILength)))
{
JLOG(j.trace()) << "Malformed transaction: invalid size of URI.";
return temMALFORMED;
}
auto const credType = tx[sfCredentialType];
if (credType.empty() || (credType.size() > maxCredentialTypeLength))
{
JLOG(j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
}
return tesSUCCESS;
}
TER
CredentialCreate::preclaim(PreclaimContext const& ctx)
{
auto const credType(ctx.tx[sfCredentialType]);
auto const subject = ctx.tx[sfSubject];
if (!ctx.view.exists(keylet::account(subject)))
{
JLOG(ctx.j.trace()) << "Subject doesn't exist.";
return tecNO_TARGET;
}
if (ctx.view.exists(keylet::credential(subject, ctx.tx[sfAccount], credType)))
{
JLOG(ctx.j.trace()) << "Credential already exists.";
return tecDUPLICATE;
}
return tesSUCCESS;
}
TER
CredentialCreate::doApply()
{
auto const subject = ctx_.tx[sfSubject];
auto const credType(ctx_.tx[sfCredentialType]);
Keylet const credentialKey = keylet::credential(subject, account_, credType);
auto const sleCred = std::make_shared<SLE>(credentialKey);
if (!sleCred)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const optExp = ctx_.tx[~sfExpiration];
if (optExp)
{
std::uint32_t const closeTime =
ctx_.view().header().parentCloseTime.time_since_epoch().count();
if (closeTime > *optExp)
{
JLOG(j_.trace()) << "Malformed transaction: "
"Expiration time is in the past.";
return tecEXPIRED;
}
sleCred->setFieldU32(sfExpiration, ctx_.tx.getFieldU32(sfExpiration));
}
auto const sleIssuer = view().peek(keylet::account(account_));
if (!sleIssuer)
return tefINTERNAL; // LCOV_EXCL_LINE
{
STAmount const reserve{
view().fees().accountReserve(sleIssuer->getFieldU32(sfOwnerCount) + 1)};
if (mPriorBalance < reserve)
return tecINSUFFICIENT_RESERVE;
}
sleCred->setAccountID(sfSubject, subject);
sleCred->setAccountID(sfIssuer, account_);
sleCred->setFieldVL(sfCredentialType, credType);
if (ctx_.tx.isFieldPresent(sfURI))
sleCred->setFieldVL(sfURI, ctx_.tx.getFieldVL(sfURI));
{
auto const page =
view().dirInsert(keylet::ownerDir(account_), credentialKey, describeOwnerDir(account_));
JLOG(j_.trace()) << "Adding Credential to owner directory " << to_string(credentialKey.key)
<< ": " << (page ? "success" : "failure");
if (!page)
return tecDIR_FULL;
sleCred->setFieldU64(sfIssuerNode, *page);
adjustOwnerCount(view(), sleIssuer, 1, j_);
}
if (subject == account_)
{
sleCred->setFieldU32(sfFlags, lsfAccepted);
}
else
{
auto const page =
view().dirInsert(keylet::ownerDir(subject), credentialKey, describeOwnerDir(subject));
JLOG(j_.trace()) << "Adding Credential to owner directory " << to_string(credentialKey.key)
<< ": " << (page ? "success" : "failure");
if (!page)
return tecDIR_FULL;
sleCred->setFieldU64(sfSubjectNode, *page);
view().update(view().peek(keylet::account(subject)));
}
view().insert(sleCred);
return tesSUCCESS;
}
// ------- DELETE --------------------------
std::uint32_t
CredentialDelete::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0;
}
NotTEC
CredentialDelete::preflight(PreflightContext const& ctx)
{
auto const subject = ctx.tx[~sfSubject];
auto const issuer = ctx.tx[~sfIssuer];
if (!subject && !issuer)
{
// Neither field is present, the transaction is malformed.
JLOG(ctx.j.trace()) << "Malformed transaction: "
"No Subject or Issuer fields.";
return temMALFORMED;
}
// Make sure that the passed account is valid.
if ((subject && subject->isZero()) || (issuer && issuer->isZero()))
{
JLOG(ctx.j.trace()) << "Malformed transaction: Subject or Issuer "
"field zeroed.";
return temINVALID_ACCOUNT_ID;
}
auto const credType = ctx.tx[sfCredentialType];
if (credType.empty() || (credType.size() > maxCredentialTypeLength))
{
JLOG(ctx.j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
}
return tesSUCCESS;
}
TER
CredentialDelete::preclaim(PreclaimContext const& ctx)
{
AccountID const account{ctx.tx[sfAccount]};
auto const subject = ctx.tx[~sfSubject].value_or(account);
auto const issuer = ctx.tx[~sfIssuer].value_or(account);
auto const credType(ctx.tx[sfCredentialType]);
if (!ctx.view.exists(keylet::credential(subject, issuer, credType)))
return tecNO_ENTRY;
return tesSUCCESS;
}
TER
CredentialDelete::doApply()
{
auto const subject = ctx_.tx[~sfSubject].value_or(account_);
auto const issuer = ctx_.tx[~sfIssuer].value_or(account_);
auto const credType(ctx_.tx[sfCredentialType]);
auto const sleCred = view().peek(keylet::credential(subject, issuer, credType));
if (!sleCred)
return tefINTERNAL; // LCOV_EXCL_LINE
if ((subject != account_) && (issuer != account_) &&
!checkExpired(sleCred, ctx_.view().header().parentCloseTime))
{
JLOG(j_.trace()) << "Can't delete non-expired credential.";
return tecNO_PERMISSION;
}
return deleteSLE(view(), sleCred, j_);
}
// ------- APPLY --------------------------
std::uint32_t
CredentialAccept::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0;
}
NotTEC
CredentialAccept::preflight(PreflightContext const& ctx)
{
if (!ctx.tx[sfIssuer])
{
JLOG(ctx.j.trace()) << "Malformed transaction: Issuer field zeroed.";
return temINVALID_ACCOUNT_ID;
}
auto const credType = ctx.tx[sfCredentialType];
if (credType.empty() || (credType.size() > maxCredentialTypeLength))
{
JLOG(ctx.j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
}
return tesSUCCESS;
}
TER
CredentialAccept::preclaim(PreclaimContext const& ctx)
{
AccountID const subject = ctx.tx[sfAccount];
AccountID const issuer = ctx.tx[sfIssuer];
auto const credType(ctx.tx[sfCredentialType]);
if (!ctx.view.exists(keylet::account(issuer)))
{
JLOG(ctx.j.warn()) << "No issuer: " << to_string(issuer);
return tecNO_ISSUER;
}
auto const sleCred = ctx.view.read(keylet::credential(subject, issuer, credType));
if (!sleCred)
{
JLOG(ctx.j.warn()) << "No credential: " << to_string(subject) << ", " << to_string(issuer)
<< ", " << credType;
return tecNO_ENTRY;
}
if (sleCred->getFieldU32(sfFlags) & lsfAccepted)
{
JLOG(ctx.j.warn()) << "Credential already accepted: " << to_string(subject) << ", "
<< to_string(issuer) << ", " << credType;
return tecDUPLICATE;
}
return tesSUCCESS;
}
TER
CredentialAccept::doApply()
{
AccountID const issuer{ctx_.tx[sfIssuer]};
// Both exist as credential object exist itself (checked in preclaim)
auto const sleSubject = view().peek(keylet::account(account_));
auto const sleIssuer = view().peek(keylet::account(issuer));
if (!sleSubject || !sleIssuer)
return tefINTERNAL; // LCOV_EXCL_LINE
{
STAmount const reserve{
view().fees().accountReserve(sleSubject->getFieldU32(sfOwnerCount) + 1)};
if (mPriorBalance < reserve)
return tecINSUFFICIENT_RESERVE;
}
auto const credType(ctx_.tx[sfCredentialType]);
Keylet const credentialKey = keylet::credential(account_, issuer, credType);
auto const sleCred = view().peek(credentialKey); // Checked in preclaim()
if (checkExpired(sleCred, view().header().parentCloseTime))
{
JLOG(j_.trace()) << "Credential is expired: " << sleCred->getText();
// delete expired credentials even if the transaction failed
auto const err = credentials::deleteSLE(view(), sleCred, j_);
return isTesSuccess(err) ? tecEXPIRED : err;
}
sleCred->setFieldU32(sfFlags, lsfAccepted);
view().update(sleCred);
adjustOwnerCount(view(), sleIssuer, -1, j_);
adjustOwnerCount(view(), sleSubject, 1, j_);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,59 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/tx/transactors/did/DIDDelete.h>
namespace xrpl {
NotTEC
DIDDelete::preflight(PreflightContext const& ctx)
{
return tesSUCCESS;
}
TER
DIDDelete::deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner)
{
auto const sle = ctx.view().peek(sleKeylet);
if (!sle)
return tecNO_ENTRY;
return DIDDelete::deleteSLE(ctx.view(), sle, owner, ctx.journal);
}
TER
DIDDelete::deleteSLE(
ApplyView& view,
std::shared_ptr<SLE> sle,
AccountID const owner,
beast::Journal j)
{
// Remove object from owner directory
if (!view.dirRemove(keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete DID Token from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const sleOwner = view.peek(keylet::account(owner));
if (!sleOwner)
return tecINTERNAL; // LCOV_EXCL_LINE
adjustOwnerCount(view, sleOwner, -1, j);
view.update(sleOwner);
// Remove object from ledger
view.erase(sle);
return tesSUCCESS;
}
TER
DIDDelete::doApply()
{
return deleteSLE(ctx_, keylet::did(account_), account_);
}
} // namespace xrpl

View File

@@ -4,7 +4,7 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/did/DID.h>
#include <xrpl/tx/transactors/did/DIDSet.h>
namespace xrpl {
@@ -134,54 +134,4 @@ DIDSet::doApply()
return addSLE(ctx_, sleDID, account_);
}
NotTEC
DIDDelete::preflight(PreflightContext const& ctx)
{
return tesSUCCESS;
}
TER
DIDDelete::deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner)
{
auto const sle = ctx.view().peek(sleKeylet);
if (!sle)
return tecNO_ENTRY;
return DIDDelete::deleteSLE(ctx.view(), sle, owner, ctx.journal);
}
TER
DIDDelete::deleteSLE(
ApplyView& view,
std::shared_ptr<SLE> sle,
AccountID const owner,
beast::Journal j)
{
// Remove object from owner directory
if (!view.dirRemove(keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete DID Token from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const sleOwner = view.peek(keylet::account(owner));
if (!sleOwner)
return tecINTERNAL; // LCOV_EXCL_LINE
adjustOwnerCount(view, sleOwner, -1, j);
view.update(sleOwner);
// Remove object from ledger
view.erase(sle);
return tesSUCCESS;
}
TER
DIDDelete::doApply()
{
return deleteSLE(ctx_, keylet::did(account_), account_);
}
} // namespace xrpl

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,201 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/tx/transactors/escrow/EscrowCancel.h>
#include <libxrpl/tx/transactors/escrow/EscrowHelpers.h>
namespace xrpl {
NotTEC
EscrowCancel::preflight(PreflightContext const& ctx)
{
return tesSUCCESS;
}
template <ValidIssueType T>
static TER
escrowCancelPreclaimHelper(
PreclaimContext const& ctx,
AccountID const& account,
STAmount const& amount);
template <>
TER
escrowCancelPreclaimHelper<Issue>(
PreclaimContext const& ctx,
AccountID const& account,
STAmount const& amount)
{
AccountID issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecINTERNAL
if (issuer == account)
return tecINTERNAL; // LCOV_EXCL_LINE
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(ctx.view, amount.issue(), account); ter != tesSUCCESS)
return ter;
return tesSUCCESS;
}
template <>
TER
escrowCancelPreclaimHelper<MPTIssue>(
PreclaimContext const& ctx,
AccountID const& account,
STAmount const& amount)
{
AccountID issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecINTERNAL
if (issuer == account)
return tecINTERNAL; // LCOV_EXCL_LINE
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth);
ter != tesSUCCESS)
return ter;
return tesSUCCESS;
}
TER
EscrowCancel::preclaim(PreclaimContext const& ctx)
{
if (ctx.view.rules().enabled(featureTokenEscrow))
{
auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]);
auto const slep = ctx.view.read(k);
if (!slep)
return tecNO_TARGET;
AccountID const account = (*slep)[sfAccount];
STAmount const amount = (*slep)[sfAmount];
if (!isXRP(amount))
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowCancelPreclaimHelper<T>(ctx, account, amount);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
}
return tesSUCCESS;
}
TER
EscrowCancel::doApply()
{
auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]);
auto const slep = ctx_.view().peek(k);
if (!slep)
{
if (ctx_.view().rules().enabled(featureTokenEscrow))
return tecINTERNAL; // LCOV_EXCL_LINE
return tecNO_TARGET;
}
auto const now = ctx_.view().header().parentCloseTime;
// No cancel time specified: can't execute at all.
if (!(*slep)[~sfCancelAfter])
return tecNO_PERMISSION;
// Too soon: can't execute before the cancel time.
if (!after(now, (*slep)[sfCancelAfter]))
return tecNO_PERMISSION;
AccountID const account = (*slep)[sfAccount];
// Remove escrow from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Remove escrow from recipient's owner directory, if present.
if (auto const optPage = (*slep)[~sfDestinationNode]; optPage)
{
if (!ctx_.view().dirRemove(keylet::ownerDir((*slep)[sfDestination]), *optPage, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
auto const sle = ctx_.view().peek(keylet::account(account));
STAmount const amount = slep->getFieldAmount(sfAmount);
// Transfer amount back to the owner
if (isXRP(amount))
(*sle)[sfBalance] = (*sle)[sfBalance] + amount;
else
{
if (!ctx_.view().rules().enabled(featureTokenEscrow))
return temDISABLED; // LCOV_EXCL_LINE
auto const issuer = amount.getIssuer();
bool const createAsset = account == account_;
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowUnlockApplyHelper<T>(
ctx_.view(),
parityRate,
slep,
mPriorBalance,
amount,
issuer,
account, // sender and receiver are the same
account,
createAsset,
j_);
},
amount.asset().value());
!isTesSuccess(ret))
return ret; // LCOV_EXCL_LINE
// Remove escrow from issuers owner directory, if present.
if (auto const optPage = (*slep)[~sfIssuerNode]; optPage)
{
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
}
adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal);
ctx_.view().update(sle);
// Remove escrow from ledger
ctx_.view().erase(slep);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,498 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/conditions/Condition.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/transactors/escrow/EscrowCreate.h>
namespace xrpl {
/*
Escrow
======
Escrow is a feature of the XRP Ledger that allows you to send conditional
XRP payments. These conditional payments, called escrows, set aside XRP and
deliver it later when certain conditions are met. Conditions to successfully
finish an escrow include time-based unlocks and crypto-conditions. Escrows
can also be set to expire if not finished in time.
The XRP set aside in an escrow is locked up. No one can use or destroy the
XRP until the escrow has been successfully finished or canceled. Before the
expiration time, only the intended receiver can get the XRP. After the
expiration time, the XRP can only be returned to the sender.
For more details on escrow, including examples, diagrams and more please
visit https://xrpl.org/escrow.html
For details on specific transactions, including fields and validation rules
please see:
`EscrowCreate`
--------------
See: https://xrpl.org/escrowcreate.html
`EscrowFinish`
--------------
See: https://xrpl.org/escrowfinish.html
`EscrowCancel`
--------------
See: https://xrpl.org/escrowcancel.html
*/
//------------------------------------------------------------------------------
TxConsequences
EscrowCreate::makeTxConsequences(PreflightContext const& ctx)
{
auto const amount = ctx.tx[sfAmount];
return TxConsequences{ctx.tx, isXRP(amount) ? amount.xrp() : beast::zero};
}
template <ValidIssueType T>
static NotTEC
escrowCreatePreflightHelper(PreflightContext const& ctx);
template <>
NotTEC
escrowCreatePreflightHelper<Issue>(PreflightContext const& ctx)
{
STAmount const amount = ctx.tx[sfAmount];
if (amount.native() || amount <= beast::zero)
return temBAD_AMOUNT;
if (badCurrency() == amount.getCurrency())
return temBAD_CURRENCY;
return tesSUCCESS;
}
template <>
NotTEC
escrowCreatePreflightHelper<MPTIssue>(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureMPTokensV1))
return temDISABLED;
auto const amount = ctx.tx[sfAmount];
if (amount.native() || amount.mpt() > MPTAmount{maxMPTokenAmount} || amount <= beast::zero)
return temBAD_AMOUNT;
return tesSUCCESS;
}
NotTEC
EscrowCreate::preflight(PreflightContext const& ctx)
{
STAmount const amount{ctx.tx[sfAmount]};
if (!isXRP(amount))
{
if (!ctx.rules.enabled(featureTokenEscrow))
return temBAD_AMOUNT;
if (auto const ret = std::visit(
[&]<typename T>(T const&) { return escrowCreatePreflightHelper<T>(ctx); },
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
else
{
if (amount <= beast::zero)
return temBAD_AMOUNT;
}
// We must specify at least one timeout value
if (!ctx.tx[~sfCancelAfter] && !ctx.tx[~sfFinishAfter])
return temBAD_EXPIRATION;
// If both finish and cancel times are specified then the cancel time must
// be strictly after the finish time.
if (ctx.tx[~sfCancelAfter] && ctx.tx[~sfFinishAfter] &&
ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter])
return temBAD_EXPIRATION;
// In the absence of a FinishAfter, the escrow can be finished
// immediately, which can be confusing. When creating an escrow,
// we want to ensure that either a FinishAfter time is explicitly
// specified or a completion condition is attached.
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition])
return temMALFORMED;
if (auto const cb = ctx.tx[~sfCondition])
{
using namespace xrpl::cryptoconditions;
std::error_code ec;
auto condition = Condition::deserialize(*cb, ec);
if (!condition)
{
JLOG(ctx.j.debug()) << "Malformed condition during escrow creation: " << ec.message();
return temMALFORMED;
}
}
return tesSUCCESS;
}
template <ValidIssueType T>
static TER
escrowCreatePreclaimHelper(
PreclaimContext const& ctx,
AccountID const& account,
AccountID const& dest,
STAmount const& amount);
template <>
TER
escrowCreatePreclaimHelper<Issue>(
PreclaimContext const& ctx,
AccountID const& account,
AccountID const& dest,
STAmount const& amount)
{
AccountID issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION
auto const sleIssuer = ctx.view.read(keylet::account(issuer));
if (!sleIssuer)
return tecNO_ISSUER;
if (!sleIssuer->isFlag(lsfAllowTrustLineLocking))
return tecNO_PERMISSION;
// If the account does not have a trustline to the issuer, return tecNO_LINE
auto const sleRippleState = ctx.view.read(keylet::line(account, issuer, amount.getCurrency()));
if (!sleRippleState)
return tecNO_LINE;
STAmount const balance = (*sleRippleState)[sfBalance];
// If balance is positive, issuer must have higher address than account
if (balance > beast::zero && issuer < account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If balance is negative, issuer must have lower address than account
if (balance < beast::zero && issuer > account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(ctx.view, amount.issue(), account); ter != tesSUCCESS)
return ter;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); ter != tesSUCCESS)
return ter;
// If the issuer has frozen the account, return tecFROZEN
if (isFrozen(ctx.view, account, amount.issue()))
return tecFROZEN;
// If the issuer has frozen the destination, return tecFROZEN
if (isFrozen(ctx.view, dest, amount.issue()))
return tecFROZEN;
STAmount const spendableAmount =
accountHolds(ctx.view, account, amount.getCurrency(), issuer, fhIGNORE_FREEZE, ctx.j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::zero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
// If the amount is not addable to the balance, return tecPRECISION_LOSS
if (!canAdd(spendableAmount, amount))
return tecPRECISION_LOSS;
return tesSUCCESS;
}
template <>
TER
escrowCreatePreclaimHelper<MPTIssue>(
PreclaimContext const& ctx,
AccountID const& account,
AccountID const& dest,
STAmount const& amount)
{
AccountID issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION
if (!sleIssuance->isFlag(lsfMPTCanEscrow))
return tecNO_PERMISSION;
// If the issuer is not the same as the issuer of the mpt, return
// tecNO_PERMISSION
if (sleIssuance->getAccountID(sfIssuer) != issuer)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, account)))
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth);
ter != tesSUCCESS)
return ter;
// If the issuer has requireAuth set, check if the destination is
// authorized
if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth);
ter != tesSUCCESS)
return ter;
// If the issuer has frozen the account, return tecLOCKED
if (isFrozen(ctx.view, account, mptIssue))
return tecLOCKED;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(ctx.view, dest, mptIssue))
return tecLOCKED;
// If the mpt cannot be transferred, return tecNO_AUTH
if (auto const ter = canTransfer(ctx.view, mptIssue, account, dest); ter != tesSUCCESS)
return ter;
STAmount const spendableAmount = accountHolds(
ctx.view, account, amount.get<MPTIssue>(), fhIGNORE_FREEZE, ahIGNORE_AUTH, ctx.j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::zero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
TER
EscrowCreate::preclaim(PreclaimContext const& ctx)
{
STAmount const amount{ctx.tx[sfAmount]};
AccountID const account{ctx.tx[sfAccount]};
AccountID const dest{ctx.tx[sfDestination]};
auto const sled = ctx.view.read(keylet::account(dest));
if (!sled)
return tecNO_DST;
// Pseudo-accounts cannot receive escrow. Note, this is not amendment-gated
// because all writes to pseudo-account discriminator fields **are**
// amendment gated, hence the behaviour of this check will always match the
// currently active amendments.
if (isPseudoAccount(sled))
return tecNO_PERMISSION;
if (!isXRP(amount))
{
if (!ctx.view.rules().enabled(featureTokenEscrow))
return temDISABLED; // LCOV_EXCL_LINE
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowCreatePreclaimHelper<T>(ctx, account, dest, amount);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
return tesSUCCESS;
}
template <ValidIssueType T>
static TER
escrowLockApplyHelper(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal);
template <>
TER
escrowLockApplyHelper<Issue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter = rippleCredit(
view, sender, issuer, amount, amount.holds<MPTIssue>() ? false : true, journal);
if (ter != tesSUCCESS)
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
template <>
TER
escrowLockApplyHelper<MPTIssue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter = rippleLockEscrowMPT(view, sender, amount, journal);
if (ter != tesSUCCESS)
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
TER
EscrowCreate::doApply()
{
auto const closeTime = ctx_.view().header().parentCloseTime;
if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter]))
return tecNO_PERMISSION;
if (ctx_.tx[~sfFinishAfter] && after(closeTime, ctx_.tx[sfFinishAfter]))
return tecNO_PERMISSION;
auto const sle = ctx_.view().peek(keylet::account(account_));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
// Check reserve and funds availability
STAmount const amount{ctx_.tx[sfAmount]};
auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1);
if (mSourceBalance < reserve)
return tecINSUFFICIENT_RESERVE;
// Check reserve and funds availability
if (isXRP(amount))
{
if (mSourceBalance < reserve + STAmount(amount).xrp())
return tecUNFUNDED;
}
// Check destination account
{
auto const sled = ctx_.view().read(keylet::account(ctx_.tx[sfDestination]));
if (!sled)
return tecNO_DST; // LCOV_EXCL_LINE
if (((*sled)[sfFlags] & lsfRequireDestTag) && !ctx_.tx[~sfDestinationTag])
return tecDST_TAG_NEEDED;
}
// Create escrow in ledger. Note that we we use the value from the
// sequence or ticket. For more explanation see comments in SeqProxy.h.
Keylet const escrowKeylet = keylet::escrow(account_, ctx_.tx.getSeqValue());
auto const slep = std::make_shared<SLE>(escrowKeylet);
(*slep)[sfAmount] = amount;
(*slep)[sfAccount] = account_;
(*slep)[~sfCondition] = ctx_.tx[~sfCondition];
(*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag];
(*slep)[sfDestination] = ctx_.tx[sfDestination];
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
(*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter];
(*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
{
(*slep)[sfSequence] = ctx_.tx.getSeqValue();
}
if (ctx_.view().rules().enabled(featureTokenEscrow) && !isXRP(amount))
{
auto const xferRate = transferRate(ctx_.view(), amount);
if (xferRate != parityRate)
(*slep)[sfTransferRate] = xferRate.value;
}
ctx_.view().insert(slep);
// Add escrow to sender's owner directory
{
auto page = ctx_.view().dirInsert(
keylet::ownerDir(account_), escrowKeylet, describeOwnerDir(account_));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfOwnerNode] = *page;
}
// If it's not a self-send, add escrow to recipient's owner directory.
AccountID const dest = ctx_.tx[sfDestination];
if (dest != account_)
{
auto page =
ctx_.view().dirInsert(keylet::ownerDir(dest), escrowKeylet, describeOwnerDir(dest));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfDestinationNode] = *page;
}
// IOU escrow objects are added to the issuer's owner directory to help
// track the total locked balance. For MPT, this isn't necessary because the
// locked balance is already stored directly in the MPTokenIssuance object.
AccountID const issuer = amount.getIssuer();
if (!isXRP(amount) && issuer != account_ && issuer != dest && !amount.holds<MPTIssue>())
{
auto page =
ctx_.view().dirInsert(keylet::ownerDir(issuer), escrowKeylet, describeOwnerDir(issuer));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfIssuerNode] = *page;
}
// Deduct owner's balance
if (isXRP(amount))
(*sle)[sfBalance] = (*sle)[sfBalance] - amount;
else
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowLockApplyHelper<T>(ctx_.view(), issuer, account_, amount, j_);
},
amount.asset().value());
!isTesSuccess(ret))
{
return ret; // LCOV_EXCL_LINE
}
}
// increment owner count
adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal);
ctx_.view().update(sle);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,372 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/conditions/Condition.h>
#include <xrpl/conditions/Fulfillment.h>
#include <xrpl/core/HashRouter.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/transactors/escrow/EscrowFinish.h>
#include <libxrpl/tx/transactors/escrow/EscrowHelpers.h>
namespace xrpl {
// During an EscrowFinish, the transaction must specify both
// a condition and a fulfillment. We track whether that
// fulfillment matches and validates the condition.
constexpr HashRouterFlags SF_CF_INVALID = HashRouterFlags::PRIVATE5;
constexpr HashRouterFlags SF_CF_VALID = HashRouterFlags::PRIVATE6;
//------------------------------------------------------------------------------
static bool
checkCondition(Slice f, Slice c)
{
using namespace xrpl::cryptoconditions;
std::error_code ec;
auto condition = Condition::deserialize(c, ec);
if (!condition)
return false;
auto fulfillment = Fulfillment::deserialize(f, ec);
if (!fulfillment)
return false;
return validate(*fulfillment, *condition);
}
bool
EscrowFinish::checkExtraFeatures(PreflightContext const& ctx)
{
return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials);
}
NotTEC
EscrowFinish::preflight(PreflightContext const& ctx)
{
auto const cb = ctx.tx[~sfCondition];
auto const fb = ctx.tx[~sfFulfillment];
// If you specify a condition, then you must also specify
// a fulfillment.
if (static_cast<bool>(cb) != static_cast<bool>(fb))
return temMALFORMED;
return tesSUCCESS;
}
NotTEC
EscrowFinish::preflightSigValidated(PreflightContext const& ctx)
{
auto const cb = ctx.tx[~sfCondition];
auto const fb = ctx.tx[~sfFulfillment];
if (cb && fb)
{
auto& router = ctx.registry.getHashRouter();
auto const id = ctx.tx.getTransactionID();
auto const flags = router.getFlags(id);
// If we haven't checked the condition, check it
// now. Whether it passes or not isn't important
// in preflight.
if (!any(flags & (SF_CF_INVALID | SF_CF_VALID)))
{
if (checkCondition(*fb, *cb))
router.setFlags(id, SF_CF_VALID);
else
router.setFlags(id, SF_CF_INVALID);
}
}
if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
return err;
return tesSUCCESS;
}
XRPAmount
EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx)
{
XRPAmount extraFee{0};
if (auto const fb = tx[~sfFulfillment])
{
extraFee += view.fees().base * (32 + (fb->size() / 16));
}
return Transactor::calculateBaseFee(view, tx) + extraFee;
}
template <ValidIssueType T>
static TER
escrowFinishPreclaimHelper(
PreclaimContext const& ctx,
AccountID const& dest,
STAmount const& amount);
template <>
TER
escrowFinishPreclaimHelper<Issue>(
PreclaimContext const& ctx,
AccountID const& dest,
STAmount const& amount)
{
AccountID issuer = amount.getIssuer();
// If the issuer is the same as the account, return tesSUCCESS
if (issuer == dest)
return tesSUCCESS;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); ter != tesSUCCESS)
return ter;
// If the issuer has deep frozen the destination, return tecFROZEN
if (isDeepFrozen(ctx.view, dest, amount.getCurrency(), amount.getIssuer()))
return tecFROZEN;
return tesSUCCESS;
}
template <>
TER
escrowFinishPreclaimHelper<MPTIssue>(
PreclaimContext const& ctx,
AccountID const& dest,
STAmount const& amount)
{
AccountID issuer = amount.getIssuer();
// If the issuer is the same as the dest, return tesSUCCESS
if (issuer == dest)
return tesSUCCESS;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the destination is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth);
ter != tesSUCCESS)
return ter;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(ctx.view, dest, mptIssue))
return tecLOCKED;
return tesSUCCESS;
}
TER
EscrowFinish::preclaim(PreclaimContext const& ctx)
{
if (ctx.view.rules().enabled(featureCredentials))
{
if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j);
!isTesSuccess(err))
return err;
}
if (ctx.view.rules().enabled(featureTokenEscrow))
{
auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]);
auto const slep = ctx.view.read(k);
if (!slep)
return tecNO_TARGET;
AccountID const dest = (*slep)[sfDestination];
STAmount const amount = (*slep)[sfAmount];
if (!isXRP(amount))
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowFinishPreclaimHelper<T>(ctx, dest, amount);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
}
return tesSUCCESS;
}
TER
EscrowFinish::doApply()
{
auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]);
auto const slep = ctx_.view().peek(k);
if (!slep)
{
if (ctx_.view().rules().enabled(featureTokenEscrow))
return tecINTERNAL; // LCOV_EXCL_LINE
return tecNO_TARGET;
}
// If a cancel time is present, a finish operation should only succeed prior
// to that time.
auto const now = ctx_.view().header().parentCloseTime;
// Too soon: can't execute before the finish time
if ((*slep)[~sfFinishAfter] && !after(now, (*slep)[sfFinishAfter]))
return tecNO_PERMISSION;
// Too late: can't execute after the cancel time
if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter]))
return tecNO_PERMISSION;
// Check cryptocondition fulfillment
{
auto const id = ctx_.tx.getTransactionID();
auto flags = ctx_.registry.getHashRouter().getFlags(id);
auto const cb = ctx_.tx[~sfCondition];
// It's unlikely that the results of the check will
// expire from the hash router, but if it happens,
// simply re-run the check.
if (cb && !any(flags & (SF_CF_INVALID | SF_CF_VALID)))
{
// LCOV_EXCL_START
auto const fb = ctx_.tx[~sfFulfillment];
if (!fb)
return tecINTERNAL;
if (checkCondition(*fb, *cb))
flags = SF_CF_VALID;
else
flags = SF_CF_INVALID;
ctx_.registry.getHashRouter().setFlags(id, flags);
// LCOV_EXCL_STOP
}
// If the check failed, then simply return an error
// and don't look at anything else.
if (any(flags & SF_CF_INVALID))
return tecCRYPTOCONDITION_ERROR;
// Check against condition in the ledger entry:
auto const cond = (*slep)[~sfCondition];
// If a condition wasn't specified during creation,
// one shouldn't be included now.
if (!cond && cb)
return tecCRYPTOCONDITION_ERROR;
// If a condition was specified during creation of
// the suspended payment, the identical condition
// must be presented again. We don't check if the
// fulfillment matches the condition since we did
// that in preflight.
if (cond && (cond != cb))
return tecCRYPTOCONDITION_ERROR;
}
// NOTE: Escrow payments cannot be used to fund accounts.
AccountID const destID = (*slep)[sfDestination];
auto const sled = ctx_.view().peek(keylet::account(destID));
if (!sled)
return tecNO_DST;
if (auto err = verifyDepositPreauth(ctx_.tx, ctx_.view(), account_, destID, sled, ctx_.journal);
!isTesSuccess(err))
return err;
AccountID const account = (*slep)[sfAccount];
// Remove escrow from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Remove escrow from recipient's owner directory, if present.
if (auto const optPage = (*slep)[~sfDestinationNode])
{
if (!ctx_.view().dirRemove(keylet::ownerDir(destID), *optPage, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
STAmount const amount = slep->getFieldAmount(sfAmount);
// Transfer amount to destination
if (isXRP(amount))
(*sled)[sfBalance] = (*sled)[sfBalance] + amount;
else
{
if (!ctx_.view().rules().enabled(featureTokenEscrow))
return temDISABLED; // LCOV_EXCL_LINE
Rate lockedRate = slep->isFieldPresent(sfTransferRate)
? xrpl::Rate(slep->getFieldU32(sfTransferRate))
: parityRate;
auto const issuer = amount.getIssuer();
bool const createAsset = destID == account_;
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowUnlockApplyHelper<T>(
ctx_.view(),
lockedRate,
sled,
mPriorBalance,
amount,
issuer,
account,
destID,
createAsset,
j_);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
// Remove escrow from issuers owner directory, if present.
if (auto const optPage = (*slep)[~sfIssuerNode]; optPage)
{
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
}
ctx_.view().update(sled);
// Adjust source owner count
auto const sle = ctx_.view().peek(keylet::account(account));
adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal);
ctx_.view().update(sle);
// Remove escrow from ledger
ctx_.view().erase(slep);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,229 @@
#pragma once
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/tx/transactors/token/MPTokenAuthorize.h>
namespace xrpl {
template <ValidIssueType T>
TER
escrowUnlockApplyHelper(
ApplyView& view,
Rate lockedRate,
std::shared_ptr<SLE> const& sleDest,
STAmount const& xrpBalance,
STAmount const& amount,
AccountID const& issuer,
AccountID const& sender,
AccountID const& receiver,
bool createAsset,
beast::Journal journal);
template <>
inline TER
escrowUnlockApplyHelper<Issue>(
ApplyView& view,
Rate lockedRate,
std::shared_ptr<SLE> const& sleDest,
STAmount const& xrpBalance,
STAmount const& amount,
AccountID const& issuer,
AccountID const& sender,
AccountID const& receiver,
bool createAsset,
beast::Journal journal)
{
Keylet const trustLineKey = keylet::line(receiver, amount.issue());
bool const recvLow = issuer > receiver;
bool const senderIssuer = issuer == sender;
bool const receiverIssuer = issuer == receiver;
bool const issuerHigh = issuer > receiver;
if (senderIssuer)
return tecINTERNAL; // LCOV_EXCL_LINE
if (receiverIssuer)
return tesSUCCESS;
if (!view.exists(trustLineKey) && createAsset && !receiverIssuer)
{
// Can the account cover the trust line's reserve?
if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)};
xrpBalance < view.fees().accountReserve(ownerCount + 1))
{
JLOG(journal.trace()) << "Trust line does not exist. "
"Insufficient reserve to create line.";
return tecNO_LINE_INSUF_RESERVE;
}
Currency const currency = amount.getCurrency();
STAmount initialBalance(amount.issue());
initialBalance.setIssuer(noAccount());
// clang-format off
if (TER const ter = trustCreate(
view, // payment sandbox
recvLow, // is dest low?
issuer, // source
receiver, // destination
trustLineKey.key, // ledger index
sleDest, // Account to add to
false, // authorize account
(sleDest->getFlags() & lsfDefaultRipple) == 0,
false, // freeze trust line
false, // deep freeze trust line
initialBalance, // zero initial balance
Issue(currency, receiver), // limit of zero
0, // quality in
0, // quality out
journal); // journal
!isTesSuccess(ter))
{
return ter; // LCOV_EXCL_LINE
}
// clang-format on
view.update(sleDest);
}
if (!view.exists(trustLineKey) && !receiverIssuer)
return tecNO_LINE;
auto const xferRate = transferRate(view, amount);
// update if issuer rate is less than locked rate
if (xferRate < lockedRate)
lockedRate = xferRate;
// Transfer Rate only applies when:
// 1. Issuer is not involved in the transfer (senderIssuer or
// receiverIssuer)
// 2. The locked rate is different from the parity rate
// NOTE: Transfer fee in escrow works a bit differently from a normal
// payment. In escrow, the fee is deducted from the locked/sending amount,
// whereas in a normal payment, the transfer fee is taken on top of the
// sending amount.
auto finalAmt = amount;
if ((!senderIssuer && !receiverIssuer) && lockedRate != parityRate)
{
// compute transfer fee, if any
auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.issue(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}
// validate the line limit if the account submitting txn is not the receiver
// of the funds
if (!createAsset)
{
auto const sleRippleState = view.peek(trustLineKey);
if (!sleRippleState)
return tecINTERNAL; // LCOV_EXCL_LINE
// if the issuer is the high, then we use the low limit
// otherwise we use the high limit
STAmount const lineLimit =
sleRippleState->getFieldAmount(issuerHigh ? sfLowLimit : sfHighLimit);
STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance);
// flip the sign of the line balance if the issuer is not high
if (!issuerHigh)
lineBalance.negate();
// add the final amount to the line balance
lineBalance += finalAmt;
// if the transfer would exceed the line limit return tecLIMIT_EXCEEDED
if (lineLimit < lineBalance)
return tecLIMIT_EXCEEDED;
}
// if destination is not the issuer then transfer funds
if (!receiverIssuer)
{
auto const ter = rippleCredit(view, issuer, receiver, finalAmt, true, journal);
if (ter != tesSUCCESS)
return ter; // LCOV_EXCL_LINE
}
return tesSUCCESS;
}
template <>
inline TER
escrowUnlockApplyHelper<MPTIssue>(
ApplyView& view,
Rate lockedRate,
std::shared_ptr<SLE> const& sleDest,
STAmount const& xrpBalance,
STAmount const& amount,
AccountID const& issuer,
AccountID const& sender,
AccountID const& receiver,
bool createAsset,
beast::Journal journal)
{
bool const senderIssuer = issuer == sender;
bool const receiverIssuer = issuer == receiver;
auto const mptID = amount.get<MPTIssue>().getMptID();
auto const issuanceKey = keylet::mptIssuance(mptID);
if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && createAsset && !receiverIssuer)
{
if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)};
xrpBalance < view.fees().accountReserve(ownerCount + 1))
{
return tecINSUFFICIENT_RESERVE;
}
if (auto const ter = MPTokenAuthorize::createMPToken(view, mptID, receiver, 0);
!isTesSuccess(ter))
{
return ter; // LCOV_EXCL_LINE
}
// update owner count.
adjustOwnerCount(view, sleDest, 1, journal);
}
if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && !receiverIssuer)
return tecNO_PERMISSION;
auto const xferRate = transferRate(view, amount);
// update if issuer rate is less than locked rate
if (xferRate < lockedRate)
lockedRate = xferRate;
// Transfer Rate only applies when:
// 1. Issuer is not involved in the transfer (senderIssuer or
// receiverIssuer)
// 2. The locked rate is different from the parity rate
// NOTE: Transfer fee in escrow works a bit differently from a normal
// payment. In escrow, the fee is deducted from the locked/sending amount,
// whereas in a normal payment, the transfer fee is taken on top of the
// sending amount.
auto finalAmt = amount;
if ((!senderIssuer && !receiverIssuer) && lockedRate != parityRate)
{
// compute transfer fee, if any
auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}
return rippleUnlockEscrowMPT(
view,
sender,
receiver,
finalAmt,
view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt,
journal);
}
} // namespace xrpl

View File

@@ -1,542 +0,0 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/tx/transactors/payment_channel/PayChan.h>
namespace xrpl {
/*
PaymentChannel
Payment channels permit off-ledger checkpoints of XRP payments flowing
in a single direction. A channel sequesters the owner's XRP in its own
ledger entry. The owner can authorize the recipient to claim up to a
given balance by giving the receiver a signed message (off-ledger). The
recipient can use this signed message to claim any unpaid balance while
the channel remains open. The owner can top off the line as needed. If
the channel has not paid out all its funds, the owner must wait out a
delay to close the channel to give the recipient a chance to supply any
claims. The recipient can close the channel at any time. Any transaction
that touches the channel after the expiration time will close the
channel. The total amount paid increases monotonically as newer claims
are issued. When the channel is closed any remaining balance is returned
to the owner. Channels are intended to permit intermittent off-ledger
settlement of ILP trust lines as balances get substantial. For
bidirectional channels, a payment channel can be used in each direction.
PaymentChannelCreate
Create a unidirectional channel. The parameters are:
Destination
The recipient at the end of the channel.
Amount
The amount of XRP to deposit in the channel immediately.
SettleDelay
The amount of time everyone but the recipient must wait for a
superior claim.
PublicKey
The key that will sign claims against the channel.
CancelAfter (optional)
Any channel transaction that touches this channel after the
`CancelAfter` time will close it.
DestinationTag (optional)
Destination tags allow the different accounts inside of a Hosted
Wallet to be mapped back onto the Ripple ledger. The destination tag
tells the server to which account in the Hosted Wallet the funds are
intended to go to. Required if the destination has lsfRequireDestTag
set.
SourceTag (optional)
Source tags allow the different accounts inside of a Hosted Wallet
to be mapped back onto the Ripple ledger. Source tags are similar to
destination tags but are for the channel owner to identify their own
transactions.
PaymentChannelFund
Add additional funds to the payment channel. Only the channel owner may
use this transaction. The parameters are:
Channel
The 256-bit ID of the channel.
Amount
The amount of XRP to add.
Expiration (optional)
Time the channel closes. The transaction will fail if the expiration
times does not satisfy the SettleDelay constraints.
PaymentChannelClaim
Place a claim against an existing channel. The parameters are:
Channel
The 256-bit ID of the channel.
Balance (optional)
The total amount of XRP delivered after this claim is processed
(optional, not needed if just closing). Amount (optional) The amount of XRP
the signature is for (not needed if equal to Balance or just closing the
line). Signature (optional) Authorization for the balance above, signed by
the owner (optional, not needed if closing or owner is performing the
transaction). The signature if for the following message: CLM\0 followed by
the 256-bit channel ID, and a 64-bit integer drops. PublicKey (optional) The
public key that made the signature (optional, required if a signature is
present) Flags tfClose Request that the channel be closed tfRenew Request
that the channel's expiration be reset. Only the owner may renew a channel.
*/
//------------------------------------------------------------------------------
static TER
closeChannel(
std::shared_ptr<SLE> const& slep,
ApplyView& view,
uint256 const& key,
beast::Journal j)
{
AccountID const src = (*slep)[sfAccount];
// Remove PayChan from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!view.dirRemove(keylet::ownerDir(src), page, key, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from src owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Remove PayChan from recipient's owner directory, if present.
if (auto const page = (*slep)[~sfDestinationNode])
{
auto const dst = (*slep)[sfDestination];
if (!view.dirRemove(keylet::ownerDir(dst), *page, key, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from dst owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Transfer amount back to owner, decrement owner count
auto const sle = view.peek(keylet::account(src));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
XRPL_ASSERT(
(*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount");
(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance];
adjustOwnerCount(view, sle, -1, j);
view.update(sle);
// Remove PayChan from ledger
view.erase(slep);
return tesSUCCESS;
}
//------------------------------------------------------------------------------
TxConsequences
PayChanCreate::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
}
NotTEC
PayChanCreate::preflight(PreflightContext const& ctx)
{
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero))
return temBAD_AMOUNT;
if (ctx.tx[sfAccount] == ctx.tx[sfDestination])
return temDST_IS_SRC;
if (!publicKeyType(ctx.tx[sfPublicKey]))
return temMALFORMED;
return tesSUCCESS;
}
TER
PayChanCreate::preclaim(PreclaimContext const& ctx)
{
auto const account = ctx.tx[sfAccount];
auto const sle = ctx.view.read(keylet::account(account));
if (!sle)
return terNO_ACCOUNT;
// Check reserve and funds availability
{
auto const balance = (*sle)[sfBalance];
auto const reserve = ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
if (balance < reserve + ctx.tx[sfAmount])
return tecUNFUNDED;
}
auto const dst = ctx.tx[sfDestination];
{
// Check destination account
auto const sled = ctx.view.read(keylet::account(dst));
if (!sled)
return tecNO_DST;
auto const flags = sled->getFlags();
// Check if they have disallowed incoming payment channels
if (flags & lsfDisallowIncomingPayChan)
return tecNO_PERMISSION;
if ((flags & lsfRequireDestTag) && !ctx.tx[~sfDestinationTag])
return tecDST_TAG_NEEDED;
// Pseudo-accounts cannot receive payment channels, other than native
// to their underlying ledger object - implemented in their respective
// transaction types. Note, this is not amendment-gated because all
// writes to pseudo-account discriminator fields **are** amendment
// gated, hence the behaviour of this check will always match the
// currently active amendments.
if (isPseudoAccount(sled))
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
TER
PayChanCreate::doApply()
{
auto const account = ctx_.tx[sfAccount];
auto const sle = ctx_.view().peek(keylet::account(account));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
if (ctx_.view().rules().enabled(fixPayChanCancelAfter))
{
auto const closeTime = ctx_.view().header().parentCloseTime;
if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter]))
return tecEXPIRED;
}
auto const dst = ctx_.tx[sfDestination];
// Create PayChan in ledger.
//
// Note that we we use the value from the sequence or ticket as the
// payChan sequence. For more explanation see comments in SeqProxy.h.
Keylet const payChanKeylet = keylet::payChan(account, dst, ctx_.tx.getSeqValue());
auto const slep = std::make_shared<SLE>(payChanKeylet);
// Funds held in this channel
(*slep)[sfAmount] = ctx_.tx[sfAmount];
// Amount channel has already paid
(*slep)[sfBalance] = ctx_.tx[sfAmount].zeroed();
(*slep)[sfAccount] = account;
(*slep)[sfDestination] = dst;
(*slep)[sfSettleDelay] = ctx_.tx[sfSettleDelay];
(*slep)[sfPublicKey] = ctx_.tx[sfPublicKey];
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
(*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag];
(*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
{
(*slep)[sfSequence] = ctx_.tx.getSeqValue();
}
ctx_.view().insert(slep);
// Add PayChan to owner directory
{
auto const page = ctx_.view().dirInsert(
keylet::ownerDir(account), payChanKeylet, describeOwnerDir(account));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfOwnerNode] = *page;
}
// Add PayChan to the recipient's owner directory
{
auto const page =
ctx_.view().dirInsert(keylet::ownerDir(dst), payChanKeylet, describeOwnerDir(dst));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfDestinationNode] = *page;
}
// Deduct owner's balance, increment owner count
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal);
ctx_.view().update(sle);
return tesSUCCESS;
}
//------------------------------------------------------------------------------
TxConsequences
PayChanFund::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
}
NotTEC
PayChanFund::preflight(PreflightContext const& ctx)
{
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero))
return temBAD_AMOUNT;
return tesSUCCESS;
}
TER
PayChanFund::doApply()
{
Keylet const k(ltPAYCHAN, ctx_.tx[sfChannel]);
auto const slep = ctx_.view().peek(k);
if (!slep)
return tecNO_ENTRY;
AccountID const src = (*slep)[sfAccount];
auto const txAccount = ctx_.tx[sfAccount];
auto const expiration = (*slep)[~sfExpiration];
{
auto const cancelAfter = (*slep)[~sfCancelAfter];
auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count();
if ((cancelAfter && closeTime >= *cancelAfter) || (expiration && closeTime >= *expiration))
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View"));
}
if (src != txAccount)
// only the owner can add funds or extend
return tecNO_PERMISSION;
if (auto extend = ctx_.tx[~sfExpiration])
{
auto minExpiration = ctx_.view().header().parentCloseTime.time_since_epoch().count() +
(*slep)[sfSettleDelay];
if (expiration && *expiration < minExpiration)
minExpiration = *expiration;
if (*extend < minExpiration)
return temBAD_EXPIRATION;
(*slep)[~sfExpiration] = *extend;
ctx_.view().update(slep);
}
auto const sle = ctx_.view().peek(keylet::account(txAccount));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
{
// Check reserve and funds availability
auto const balance = (*sle)[sfBalance];
auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
if (balance < reserve + ctx_.tx[sfAmount])
return tecUNFUNDED;
}
// do not allow adding funds if dst does not exist
if (AccountID const dst = (*slep)[sfDestination]; !ctx_.view().read(keylet::account(dst)))
{
return tecNO_DST;
}
(*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount];
ctx_.view().update(slep);
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
ctx_.view().update(sle);
return tesSUCCESS;
}
//------------------------------------------------------------------------------
bool
PayChanClaim::checkExtraFeatures(PreflightContext const& ctx)
{
return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials);
}
std::uint32_t
PayChanClaim::getFlagsMask(PreflightContext const&)
{
return tfPaymentChannelClaimMask;
}
NotTEC
PayChanClaim::preflight(PreflightContext const& ctx)
{
auto const bal = ctx.tx[~sfBalance];
if (bal && (!isXRP(*bal) || *bal <= beast::zero))
return temBAD_AMOUNT;
auto const amt = ctx.tx[~sfAmount];
if (amt && (!isXRP(*amt) || *amt <= beast::zero))
return temBAD_AMOUNT;
if (bal && amt && *bal > *amt)
return temBAD_AMOUNT;
{
auto const flags = ctx.tx.getFlags();
if ((flags & tfClose) && (flags & tfRenew))
return temMALFORMED;
}
if (auto const sig = ctx.tx[~sfSignature])
{
if (!(ctx.tx[~sfPublicKey] && bal))
return temMALFORMED;
// Check the signature
// The signature isn't needed if txAccount == src, but if it's
// present, check it
auto const reqBalance = bal->xrp();
auto const authAmt = amt ? amt->xrp() : reqBalance;
if (reqBalance > authAmt)
return temBAD_AMOUNT;
Keylet const k(ltPAYCHAN, ctx.tx[sfChannel]);
if (!publicKeyType(ctx.tx[sfPublicKey]))
return temMALFORMED;
PublicKey const pk(ctx.tx[sfPublicKey]);
Serializer msg;
serializePayChanAuthorization(msg, k.key, authAmt);
if (!verify(pk, msg.slice(), *sig))
return temBAD_SIGNATURE;
}
if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
return err;
return tesSUCCESS;
}
TER
PayChanClaim::preclaim(PreclaimContext const& ctx)
{
if (!ctx.view.rules().enabled(featureCredentials))
return Transactor::preclaim(ctx);
if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j);
!isTesSuccess(err))
return err;
return tesSUCCESS;
}
TER
PayChanClaim::doApply()
{
Keylet const k(ltPAYCHAN, ctx_.tx[sfChannel]);
auto const slep = ctx_.view().peek(k);
if (!slep)
return tecNO_TARGET;
AccountID const src = (*slep)[sfAccount];
AccountID const dst = (*slep)[sfDestination];
AccountID const txAccount = ctx_.tx[sfAccount];
auto const curExpiration = (*slep)[~sfExpiration];
{
auto const cancelAfter = (*slep)[~sfCancelAfter];
auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count();
if ((cancelAfter && closeTime >= *cancelAfter) ||
(curExpiration && closeTime >= *curExpiration))
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View"));
}
if (txAccount != src && txAccount != dst)
return tecNO_PERMISSION;
if (ctx_.tx[~sfBalance])
{
auto const chanBalance = slep->getFieldAmount(sfBalance).xrp();
auto const chanFunds = slep->getFieldAmount(sfAmount).xrp();
auto const reqBalance = ctx_.tx[sfBalance].xrp();
if (txAccount == dst && !ctx_.tx[~sfSignature])
return temBAD_SIGNATURE;
if (ctx_.tx[~sfSignature])
{
PublicKey const pk((*slep)[sfPublicKey]);
if (ctx_.tx[sfPublicKey] != pk)
return temBAD_SIGNER;
}
if (reqBalance > chanFunds)
return tecUNFUNDED_PAYMENT;
if (reqBalance <= chanBalance)
// nothing requested
return tecUNFUNDED_PAYMENT;
auto const sled = ctx_.view().peek(keylet::account(dst));
if (!sled)
return tecNO_DST;
if (auto err =
verifyDepositPreauth(ctx_.tx, ctx_.view(), txAccount, dst, sled, ctx_.journal);
!isTesSuccess(err))
return err;
(*slep)[sfBalance] = ctx_.tx[sfBalance];
XRPAmount const reqDelta = reqBalance - chanBalance;
XRPL_ASSERT(reqDelta >= beast::zero, "xrpl::PayChanClaim::doApply : minimum balance delta");
(*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta;
ctx_.view().update(sled);
ctx_.view().update(slep);
}
if (ctx_.tx.getFlags() & tfRenew)
{
if (src != txAccount)
return tecNO_PERMISSION;
(*slep)[~sfExpiration] = std::nullopt;
ctx_.view().update(slep);
}
if (ctx_.tx.getFlags() & tfClose)
{
// Channel will close immediately if dry or the receiver closes
if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount])
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View"));
auto const settleExpiration =
ctx_.view().header().parentCloseTime.time_since_epoch().count() +
(*slep)[sfSettleDelay];
if (!curExpiration || *curExpiration > settleExpiration)
{
(*slep)[~sfExpiration] = settleExpiration;
ctx_.view().update(slep);
}
}
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,186 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/payment_channel/PayChanClaim.h>
#include <libxrpl/tx/transactors/payment_channel/PayChanHelpers.h>
namespace xrpl {
bool
PayChanClaim::checkExtraFeatures(PreflightContext const& ctx)
{
return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials);
}
std::uint32_t
PayChanClaim::getFlagsMask(PreflightContext const&)
{
return tfPaymentChannelClaimMask;
}
NotTEC
PayChanClaim::preflight(PreflightContext const& ctx)
{
auto const bal = ctx.tx[~sfBalance];
if (bal && (!isXRP(*bal) || *bal <= beast::zero))
return temBAD_AMOUNT;
auto const amt = ctx.tx[~sfAmount];
if (amt && (!isXRP(*amt) || *amt <= beast::zero))
return temBAD_AMOUNT;
if (bal && amt && *bal > *amt)
return temBAD_AMOUNT;
{
auto const flags = ctx.tx.getFlags();
if ((flags & tfClose) && (flags & tfRenew))
return temMALFORMED;
}
if (auto const sig = ctx.tx[~sfSignature])
{
if (!(ctx.tx[~sfPublicKey] && bal))
return temMALFORMED;
// Check the signature
// The signature isn't needed if txAccount == src, but if it's
// present, check it
auto const reqBalance = bal->xrp();
auto const authAmt = amt ? amt->xrp() : reqBalance;
if (reqBalance > authAmt)
return temBAD_AMOUNT;
Keylet const k(ltPAYCHAN, ctx.tx[sfChannel]);
if (!publicKeyType(ctx.tx[sfPublicKey]))
return temMALFORMED;
PublicKey const pk(ctx.tx[sfPublicKey]);
Serializer msg;
serializePayChanAuthorization(msg, k.key, authAmt);
if (!verify(pk, msg.slice(), *sig))
return temBAD_SIGNATURE;
}
if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
return err;
return tesSUCCESS;
}
TER
PayChanClaim::preclaim(PreclaimContext const& ctx)
{
if (!ctx.view.rules().enabled(featureCredentials))
return Transactor::preclaim(ctx);
if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j);
!isTesSuccess(err))
return err;
return tesSUCCESS;
}
TER
PayChanClaim::doApply()
{
Keylet const k(ltPAYCHAN, ctx_.tx[sfChannel]);
auto const slep = ctx_.view().peek(k);
if (!slep)
return tecNO_TARGET;
AccountID const src = (*slep)[sfAccount];
AccountID const dst = (*slep)[sfDestination];
AccountID const txAccount = ctx_.tx[sfAccount];
auto const curExpiration = (*slep)[~sfExpiration];
{
auto const cancelAfter = (*slep)[~sfCancelAfter];
auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count();
if ((cancelAfter && closeTime >= *cancelAfter) ||
(curExpiration && closeTime >= *curExpiration))
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View"));
}
if (txAccount != src && txAccount != dst)
return tecNO_PERMISSION;
if (ctx_.tx[~sfBalance])
{
auto const chanBalance = slep->getFieldAmount(sfBalance).xrp();
auto const chanFunds = slep->getFieldAmount(sfAmount).xrp();
auto const reqBalance = ctx_.tx[sfBalance].xrp();
if (txAccount == dst && !ctx_.tx[~sfSignature])
return temBAD_SIGNATURE;
if (ctx_.tx[~sfSignature])
{
PublicKey const pk((*slep)[sfPublicKey]);
if (ctx_.tx[sfPublicKey] != pk)
return temBAD_SIGNER;
}
if (reqBalance > chanFunds)
return tecUNFUNDED_PAYMENT;
if (reqBalance <= chanBalance)
// nothing requested
return tecUNFUNDED_PAYMENT;
auto const sled = ctx_.view().peek(keylet::account(dst));
if (!sled)
return tecNO_DST;
if (auto err =
verifyDepositPreauth(ctx_.tx, ctx_.view(), txAccount, dst, sled, ctx_.journal);
!isTesSuccess(err))
return err;
(*slep)[sfBalance] = ctx_.tx[sfBalance];
XRPAmount const reqDelta = reqBalance - chanBalance;
XRPL_ASSERT(reqDelta >= beast::zero, "xrpl::PayChanClaim::doApply : minimum balance delta");
(*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta;
ctx_.view().update(sled);
ctx_.view().update(slep);
}
if (ctx_.tx.getFlags() & tfRenew)
{
if (src != txAccount)
return tecNO_PERMISSION;
(*slep)[~sfExpiration] = std::nullopt;
ctx_.view().update(slep);
}
if (ctx_.tx.getFlags() & tfClose)
{
// Channel will close immediately if dry or the receiver closes
if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount])
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View"));
auto const settleExpiration =
ctx_.view().header().parentCloseTime.time_since_epoch().count() +
(*slep)[sfSettleDelay];
if (!curExpiration || *curExpiration > settleExpiration)
{
(*slep)[~sfExpiration] = settleExpiration;
ctx_.view().update(slep);
}
}
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,173 @@
#include <xrpl/basics/chrono.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/transactors/payment_channel/PayChanCreate.h>
namespace xrpl {
/*
PaymentChannel
Payment channels permit off-ledger checkpoints of XRP payments flowing
in a single direction. A channel sequesters the owner's XRP in its own
ledger entry. The owner can authorize the recipient to claim up to a
given balance by giving the receiver a signed message (off-ledger). The
recipient can use this signed message to claim any unpaid balance while
the channel remains open. The owner can top off the line as needed. If
the channel has not paid out all its funds, the owner must wait out a
delay to close the channel to give the recipient a chance to supply any
claims. The recipient can close the channel at any time. Any transaction
that touches the channel after the expiration time will close the
channel. The total amount paid increases monotonically as newer claims
are issued. When the channel is closed any remaining balance is returned
to the owner. Channels are intended to permit intermittent off-ledger
settlement of ILP trust lines as balances get substantial. For
bidirectional channels, a payment channel can be used in each direction.
*/
//------------------------------------------------------------------------------
TxConsequences
PayChanCreate::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
}
NotTEC
PayChanCreate::preflight(PreflightContext const& ctx)
{
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero))
return temBAD_AMOUNT;
if (ctx.tx[sfAccount] == ctx.tx[sfDestination])
return temDST_IS_SRC;
if (!publicKeyType(ctx.tx[sfPublicKey]))
return temMALFORMED;
return tesSUCCESS;
}
TER
PayChanCreate::preclaim(PreclaimContext const& ctx)
{
auto const account = ctx.tx[sfAccount];
auto const sle = ctx.view.read(keylet::account(account));
if (!sle)
return terNO_ACCOUNT;
// Check reserve and funds availability
{
auto const balance = (*sle)[sfBalance];
auto const reserve = ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
if (balance < reserve + ctx.tx[sfAmount])
return tecUNFUNDED;
}
auto const dst = ctx.tx[sfDestination];
{
// Check destination account
auto const sled = ctx.view.read(keylet::account(dst));
if (!sled)
return tecNO_DST;
auto const flags = sled->getFlags();
// Check if they have disallowed incoming payment channels
if (flags & lsfDisallowIncomingPayChan)
return tecNO_PERMISSION;
if ((flags & lsfRequireDestTag) && !ctx.tx[~sfDestinationTag])
return tecDST_TAG_NEEDED;
// Pseudo-accounts cannot receive payment channels, other than native
// to their underlying ledger object - implemented in their respective
// transaction types. Note, this is not amendment-gated because all
// writes to pseudo-account discriminator fields **are** amendment
// gated, hence the behaviour of this check will always match the
// currently active amendments.
if (isPseudoAccount(sled))
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
TER
PayChanCreate::doApply()
{
auto const account = ctx_.tx[sfAccount];
auto const sle = ctx_.view().peek(keylet::account(account));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
if (ctx_.view().rules().enabled(fixPayChanCancelAfter))
{
auto const closeTime = ctx_.view().header().parentCloseTime;
if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter]))
return tecEXPIRED;
}
auto const dst = ctx_.tx[sfDestination];
// Create PayChan in ledger.
//
// Note that we we use the value from the sequence or ticket as the
// payChan sequence. For more explanation see comments in SeqProxy.h.
Keylet const payChanKeylet = keylet::payChan(account, dst, ctx_.tx.getSeqValue());
auto const slep = std::make_shared<SLE>(payChanKeylet);
// Funds held in this channel
(*slep)[sfAmount] = ctx_.tx[sfAmount];
// Amount channel has already paid
(*slep)[sfBalance] = ctx_.tx[sfAmount].zeroed();
(*slep)[sfAccount] = account;
(*slep)[sfDestination] = dst;
(*slep)[sfSettleDelay] = ctx_.tx[sfSettleDelay];
(*slep)[sfPublicKey] = ctx_.tx[sfPublicKey];
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
(*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag];
(*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
{
(*slep)[sfSequence] = ctx_.tx.getSeqValue();
}
ctx_.view().insert(slep);
// Add PayChan to owner directory
{
auto const page = ctx_.view().dirInsert(
keylet::ownerDir(account), payChanKeylet, describeOwnerDir(account));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfOwnerNode] = *page;
}
// Add PayChan to the recipient's owner directory
{
auto const page =
ctx_.view().dirInsert(keylet::ownerDir(dst), payChanKeylet, describeOwnerDir(dst));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfDestinationNode] = *page;
}
// Deduct owner's balance, increment owner count
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal);
ctx_.view().update(sle);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,92 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/tx/transactors/payment_channel/PayChanFund.h>
#include <libxrpl/tx/transactors/payment_channel/PayChanHelpers.h>
namespace xrpl {
TxConsequences
PayChanFund::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
}
NotTEC
PayChanFund::preflight(PreflightContext const& ctx)
{
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero))
return temBAD_AMOUNT;
return tesSUCCESS;
}
TER
PayChanFund::doApply()
{
Keylet const k(ltPAYCHAN, ctx_.tx[sfChannel]);
auto const slep = ctx_.view().peek(k);
if (!slep)
return tecNO_ENTRY;
AccountID const src = (*slep)[sfAccount];
auto const txAccount = ctx_.tx[sfAccount];
auto const expiration = (*slep)[~sfExpiration];
{
auto const cancelAfter = (*slep)[~sfCancelAfter];
auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count();
if ((cancelAfter && closeTime >= *cancelAfter) || (expiration && closeTime >= *expiration))
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View"));
}
if (src != txAccount)
// only the owner can add funds or extend
return tecNO_PERMISSION;
if (auto extend = ctx_.tx[~sfExpiration])
{
auto minExpiration = ctx_.view().header().parentCloseTime.time_since_epoch().count() +
(*slep)[sfSettleDelay];
if (expiration && *expiration < minExpiration)
minExpiration = *expiration;
if (*extend < minExpiration)
return temBAD_EXPIRATION;
(*slep)[~sfExpiration] = *extend;
ctx_.view().update(slep);
}
auto const sle = ctx_.view().peek(keylet::account(txAccount));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
{
// Check reserve and funds availability
auto const balance = (*sle)[sfBalance];
auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
if (balance < reserve + ctx_.tx[sfAmount])
return tecUNFUNDED;
}
// do not allow adding funds if dst does not exist
if (AccountID const dst = (*slep)[sfDestination]; !ctx_.view().read(keylet::account(dst)))
{
return tecNO_DST;
}
(*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount];
ctx_.view().update(slep);
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
ctx_.view().update(sle);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,58 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Indexes.h>
#include <libxrpl/tx/transactors/payment_channel/PayChanHelpers.h>
namespace xrpl {
TER
closeChannel(
std::shared_ptr<SLE> const& slep,
ApplyView& view,
uint256 const& key,
beast::Journal j)
{
AccountID const src = (*slep)[sfAccount];
// Remove PayChan from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!view.dirRemove(keylet::ownerDir(src), page, key, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from src owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Remove PayChan from recipient's owner directory, if present.
if (auto const page = (*slep)[~sfDestinationNode])
{
auto const dst = (*slep)[sfDestination];
if (!view.dirRemove(keylet::ownerDir(dst), *page, key, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from dst owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Transfer amount back to owner, decrement owner count
auto const sle = view.peek(keylet::account(src));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
XRPL_ASSERT(
(*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount");
(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance];
adjustOwnerCount(view, sle, -1, j);
view.update(sle);
// Remove PayChan from ledger
view.erase(slep);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,15 @@
#pragma once
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/protocol/UintTypes.h>
namespace xrpl {
TER
closeChannel(
std::shared_ptr<SLE> const& slep,
ApplyView& view,
uint256 const& key,
beast::Journal j);
} // namespace xrpl

View File

@@ -1,160 +0,0 @@
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/token/ConfidentialMPTClawback.h>
namespace xrpl {
NotTEC
ConfidentialMPTClawback::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
auto const account = ctx.tx[sfAccount];
// Only issuer can clawback
if (account != MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer())
return temMALFORMED;
// Cannot clawback from self
if (account == ctx.tx[sfHolder])
return temMALFORMED;
// Check invalid claw amount
auto const clawAmount = ctx.tx[sfMPTAmount];
if (clawAmount == 0 || clawAmount > maxMPTokenAmount)
return temBAD_AMOUNT;
// Verify proof length
if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
return temMALFORMED;
return tesSUCCESS;
}
TER
ConfidentialMPTClawback::preclaim(PreclaimContext const& ctx)
{
// Check if sender account exists
auto const account = ctx.tx[sfAccount];
if (!ctx.view.exists(keylet::account(account)))
return terNO_ACCOUNT;
// Check if holder account exists
auto const holder = ctx.tx[sfHolder];
if (!ctx.view.exists(keylet::account(holder)))
return tecNO_TARGET;
// Check if MPT issuance exists
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// Sanity check: issuer must be the same as account
if (sleIssuance->getAccountID(sfIssuer) != account)
return tefINTERNAL; // LCOV_EXCL_LINE
// Check if issuance has issuer ElGamal public key
if (!sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
return tecNO_PERMISSION;
// Check if clawback is allowed
if (!sleIssuance->isFlag(lsfMPTCanClawback))
return tecNO_PERMISSION;
// Check holder's MPToken
auto const sleHolderMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, holder));
if (!sleHolderMPToken)
return tecOBJECT_NOT_FOUND;
// Check if holder has confidential balances to claw back
if (!sleHolderMPToken->isFieldPresent(sfIssuerEncryptedBalance))
return tecNO_PERMISSION;
// Sanity check: claw amount can not exceed confidential outstanding amount
auto const amount = ctx.tx[sfMPTAmount];
if (amount > (*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0))
return tecINSUFFICIENT_FUNDS;
auto const contextHash =
getClawbackContextHash(account, mptIssuanceID, ctx.tx.getSeqProxy().value(), holder);
// Verify the revealed confidential amount by the issuer matches the exact
// confidential balance of the holder.
return verifyClawbackEqualityProof(
amount,
ctx.tx[sfZKProof],
(*sleIssuance)[sfIssuerEncryptionKey],
(*sleHolderMPToken)[sfIssuerEncryptedBalance],
contextHash);
}
TER
ConfidentialMPTClawback::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto const holder = ctx_.tx[sfHolder];
auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
auto sleHolderMPToken = view().peek(keylet::mptoken(mptIssuanceID, holder));
if (!sleIssuance || !sleHolderMPToken)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const clawAmount = ctx_.tx[sfMPTAmount];
Slice const holderPubKey = (*sleHolderMPToken)[sfHolderEncryptionKey];
Slice const issuerPubKey = (*sleIssuance)[sfIssuerEncryptionKey];
// After clawback, the balance should be encrypted zero.
auto const encZeroForHolder = encryptCanonicalZeroAmount(holderPubKey, holder, mptIssuanceID);
if (!encZeroForHolder)
return tecINTERNAL; // LCOV_EXCL_LINE
auto encZeroForIssuer = encryptCanonicalZeroAmount(issuerPubKey, holder, mptIssuanceID);
if (!encZeroForIssuer)
return tecINTERNAL; // LCOV_EXCL_LINE
// Set holder's confidential balances to encrypted zero
(*sleHolderMPToken)[sfConfidentialBalanceInbox] = *encZeroForHolder;
(*sleHolderMPToken)[sfConfidentialBalanceSpending] = *encZeroForHolder;
(*sleHolderMPToken)[sfIssuerEncryptedBalance] = std::move(*encZeroForIssuer);
incrementConfidentialVersion(*sleHolderMPToken);
if (sleHolderMPToken->isFieldPresent(sfAuditorEncryptedBalance))
{
// Sanity check: the issuance must have an auditor public key if
// auditing is enabled.
if (!sleIssuance->isFieldPresent(sfAuditorEncryptionKey))
return tecINTERNAL; // LCOV_EXCL_LINE
Slice const auditorPubKey = (*sleIssuance)[sfAuditorEncryptionKey];
auto encZeroForAuditor = encryptCanonicalZeroAmount(auditorPubKey, holder, mptIssuanceID);
if (!encZeroForAuditor)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleHolderMPToken)[sfAuditorEncryptedBalance] = std::move(*encZeroForAuditor);
}
// Decrease Global Confidential Outstanding Amount
auto const oldCOA = (*sleIssuance)[sfConfidentialOutstandingAmount];
(*sleIssuance)[sfConfidentialOutstandingAmount] = oldCOA - clawAmount;
// Decrease Global Total Outstanding Amount
auto const oldOA = (*sleIssuance)[sfOutstandingAmount];
(*sleIssuance)[sfOutstandingAmount] = oldOA - clawAmount;
view().update(sleHolderMPToken);
view().update(sleIssuance);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -1,253 +0,0 @@
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/token/ConfidentialMPTConvert.h>
namespace xrpl {
NotTEC
ConfidentialMPTConvert::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
// issuer cannot convert
if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
return temMALFORMED;
if (ctx.tx[sfMPTAmount] > maxMPTokenAmount)
return temBAD_AMOUNT;
if (ctx.tx[sfBlindingFactor].size() != ecBlindingFactorLength)
return temMALFORMED;
if (ctx.tx.isFieldPresent(sfHolderEncryptionKey))
{
if (!isValidCompressedECPoint(ctx.tx[sfHolderEncryptionKey]))
return temMALFORMED;
// proof of knowledge of the secret key corresponding to the provided
// public key is needed when holder ec public key is being set.
if (!ctx.tx.isFieldPresent(sfZKProof))
return temMALFORMED;
// verify schnorr proof length when registerring holder ec public key
if (ctx.tx[sfZKProof].size() != ecSchnorrProofLength)
return temMALFORMED;
}
else
{
// zkp should not be present if public key was already set
if (ctx.tx.isFieldPresent(sfZKProof))
return temMALFORMED;
}
// check encrypted amount format after the above basic checks
// this check is more expensive so put it at the end
if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res))
return res;
return tesSUCCESS;
}
TER
ConfidentialMPTConvert::preclaim(PreclaimContext const& ctx)
{
auto const account = ctx.tx[sfAccount];
auto const issuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const amount = ctx.tx[sfMPTAmount];
// ensure that issuance exists
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(issuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanConfidentialAmount))
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on the
// issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == account)
return tefINTERNAL; // LCOV_EXCL_LINE
// issuer has not uploaded their pub key yet
if (!sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
return tecNO_PERMISSION;
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey);
// tx must include auditor ciphertext if the issuance has enabled
// auditing, and must not include it if auditing is not enabled
if (requiresAuditor != hasAuditor)
return tecNO_PERMISSION;
auto const sleMptoken = ctx.view.read(keylet::mptoken(issuanceID, account));
if (!sleMptoken)
return tecOBJECT_NOT_FOUND;
auto const mptIssue = MPTIssue{issuanceID};
STAmount const mptAmount =
STAmount(MPTAmount{static_cast<MPTAmount::value_type>(amount)}, mptIssue);
if (accountHolds(
ctx.view,
account,
mptIssue,
FreezeHandling::fhZERO_IF_FROZEN,
AuthHandling::ahZERO_IF_UNAUTHORIZED,
ctx.j) < mptAmount)
{
return tecINSUFFICIENT_FUNDS;
}
auto const hasHolderKeyOnLedger = sleMptoken->isFieldPresent(sfHolderEncryptionKey);
auto const hasHolderKeyInTx = ctx.tx.isFieldPresent(sfHolderEncryptionKey);
// must have pk to convert
if (!hasHolderKeyOnLedger && !hasHolderKeyInTx)
return tecNO_PERMISSION;
// can't update if there's already a pk
if (hasHolderKeyOnLedger && hasHolderKeyInTx)
return tecDUPLICATE;
Slice holderPubKey;
if (hasHolderKeyInTx)
{
holderPubKey = ctx.tx[sfHolderEncryptionKey];
auto const contextHash =
getConvertContextHash(account, issuanceID, ctx.tx.getSeqProxy().value());
// when register new pk, verify through schnorr proof
if (!isTesSuccess(verifySchnorrProof(holderPubKey, ctx.tx[sfZKProof], contextHash)))
{
return tecBAD_PROOF;
}
}
else
{
holderPubKey = (*sleMptoken)[sfHolderEncryptionKey];
}
std::optional<ConfidentialRecipient> auditor;
if (hasAuditor)
{
auditor.emplace(
ConfidentialRecipient{
(*sleIssuance)[sfAuditorEncryptionKey], ctx.tx[sfAuditorEncryptedAmount]});
}
return verifyRevealedAmount(
amount,
ctx.tx[sfBlindingFactor],
{holderPubKey, ctx.tx[sfHolderEncryptedAmount]},
{(*sleIssuance)[sfIssuerEncryptionKey], ctx.tx[sfIssuerEncryptedAmount]},
auditor);
}
TER
ConfidentialMPTConvert::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
if (!sleMptoken)
return tecINTERNAL; // LCOV_EXCL_LINE
auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const amtToConvert = ctx_.tx[sfMPTAmount];
auto const amt = (*sleMptoken)[~sfMPTAmount].value_or(0);
if (ctx_.tx.isFieldPresent(sfHolderEncryptionKey))
(*sleMptoken)[sfHolderEncryptionKey] = ctx_.tx[sfHolderEncryptionKey];
// Converting decreases regular balance and increases confidential outstanding.
// The confidential outstanding tracks total tokens in confidential form globally.
(*sleMptoken)[sfMPTAmount] = amt - amtToConvert;
(*sleIssuance)[sfConfidentialOutstandingAmount] =
(*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) + amtToConvert;
Slice const holderEc = ctx_.tx[sfHolderEncryptedAmount];
Slice const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
// Two cases for Convert:
// 1. Holder already has confidential balances -> homomorphically add to inbox
// 2. First-time convert -> initialize all confidential balance fields
if (sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
{
// Case 1: Add to existing inbox balance (holder will merge later)
{
auto sum = homomorphicAdd(holderEc, (*sleMptoken)[sfConfidentialBalanceInbox]);
if (!sum)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfConfidentialBalanceInbox] = std::move(*sum);
}
// homomorphically add issuer's encrypted balance
{
auto sum = homomorphicAdd(issuerEc, (*sleMptoken)[sfIssuerEncryptedBalance]);
if (!sum)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfIssuerEncryptedBalance] = std::move(*sum);
}
// homomorphically add auditor's encrypted balance
if (auditorEc)
{
auto sum = homomorphicAdd(*auditorEc, (*sleMptoken)[sfAuditorEncryptedBalance]);
if (!sum)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfAuditorEncryptedBalance] = std::move(*sum);
}
}
else if (
!sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
{
// Case 2: First-time convert - initialize all confidential fields
(*sleMptoken)[sfConfidentialBalanceInbox] = holderEc;
(*sleMptoken)[sfIssuerEncryptedBalance] = issuerEc;
(*sleMptoken)[sfConfidentialBalanceVersion] = 0;
if (auditorEc)
(*sleMptoken)[sfAuditorEncryptedBalance] = *auditorEc;
// Spending balance starts at zero. Must use canonical zero encryption
// (deterministic ciphertext) so the ledger state is reproducible.
auto zeroBalance = encryptCanonicalZeroAmount(
(*sleMptoken)[sfHolderEncryptionKey], account_, mptIssuanceID);
if (!zeroBalance)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*zeroBalance);
}
else
{
// both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should
// exist together
return tecINTERNAL; // LCOV_EXCL_LINE
}
view().update(sleIssuance);
view().update(sleMptoken);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -1,294 +0,0 @@
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h>
#include <cstddef>
namespace xrpl {
NotTEC
ConfidentialMPTConvertBack::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
// issuer cannot convert back
if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
return temMALFORMED;
if (ctx.tx[sfMPTAmount] == 0 || ctx.tx[sfMPTAmount] > maxMPTokenAmount)
return temBAD_AMOUNT;
if (ctx.tx[sfBlindingFactor].size() != ecBlindingFactorLength)
return temMALFORMED;
if (!isValidCompressedECPoint(ctx.tx[sfBalanceCommitment]))
return temMALFORMED;
// check encrypted amount format after the above basic checks
// this check is more expensive so put it at the end
if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res))
return res;
// ConvertBack proof = pedersen linkage proof + single bulletproof
if (ctx.tx[sfZKProof].size() != ecPedersenProofLength + ecSingleBulletproofLength)
return temMALFORMED;
return tesSUCCESS;
}
/**
* Verifies the cryptographic proofs for a ConvertBack transaction.
*
* This function verifies three proofs:
* 1. Revealed amount proof: verifies the encrypted amounts (holder, issuer,
* auditor) all encrypt the same revealed amount using the blinding factor.
* 2. Pedersen linkage proof: verifies the balance commitment is derived from
* the holder's encrypted spending balance.
* 3. Bulletproof (range proof): verifies the remaining balance (balance - amount)
* is non-negative, preventing overdrafts.
*
* All proofs are verified before returning any error to prevent timing attacks.
*/
static TER
verifyProofs(
STTx const& tx,
std::shared_ptr<SLE const> const& issuance,
std::shared_ptr<SLE const> const& mptoken)
{
if (!mptoken->isFieldPresent(sfHolderEncryptionKey))
return tecINTERNAL; // LCOV_EXCL_LINE
auto const mptIssuanceID = tx[sfMPTokenIssuanceID];
auto const account = tx[sfAccount];
auto const amount = tx[sfMPTAmount];
auto const blindingFactor = tx[sfBlindingFactor];
auto const holderPubKey = (*mptoken)[sfHolderEncryptionKey];
auto const contextHash = getConvertBackContextHash(
account,
mptIssuanceID,
tx.getSeqProxy().value(),
(*mptoken)[~sfConfidentialBalanceVersion].value_or(0));
// Prepare Auditor Info
std::optional<ConfidentialRecipient> auditor;
bool const hasAuditor = issuance->isFieldPresent(sfAuditorEncryptionKey);
if (hasAuditor)
{
auditor.emplace(
ConfidentialRecipient{
(*issuance)[sfAuditorEncryptionKey], tx[sfAuditorEncryptedAmount]});
}
// Run all verifications before returning any error to prevent timing attacks
// that could reveal which proof failed.
bool valid = true;
// verify revealed amount
if (auto const ter = verifyRevealedAmount(
amount,
blindingFactor,
{holderPubKey, tx[sfHolderEncryptedAmount]},
{(*issuance)[sfIssuerEncryptionKey], tx[sfIssuerEncryptedAmount]},
auditor);
!isTesSuccess(ter))
{
valid = false;
}
// Parse proof components using offset
auto const proof = tx[sfZKProof];
size_t remainingLength = proof.size();
size_t currentOffset = 0;
// Extract Pedersen linkage proof
if (remainingLength < ecPedersenProofLength)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const pedersenProof = proof.substr(currentOffset, ecPedersenProofLength);
currentOffset += ecPedersenProofLength;
remainingLength -= ecPedersenProofLength;
// Extract bulletproof
if (remainingLength < ecSingleBulletproofLength)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const bulletproof = proof.substr(currentOffset, ecSingleBulletproofLength);
currentOffset += ecSingleBulletproofLength;
remainingLength -= ecSingleBulletproofLength;
if (remainingLength != 0)
return tecINTERNAL; // LCOV_EXCL_LINE
// verify el gamal pedersen linkage
if (auto const ter = verifyBalancePcmLinkage(
pedersenProof,
(*mptoken)[sfConfidentialBalanceSpending],
holderPubKey,
tx[sfBalanceCommitment],
contextHash);
!isTesSuccess(ter))
{
valid = false;
}
// verify bullet proof
{
// Compute PC_rem = PC_balance - mG (the commitment to the remaining balance)
Buffer pcRem;
if (auto const ter = computeConvertBackRemainder(tx[sfBalanceCommitment], amount, pcRem);
!isTesSuccess(ter))
{
valid = false;
}
// The bulletproof verifies that the remaining balance is non-negative
std::vector<Slice> commitments{Slice(pcRem.data(), pcRem.size())};
if (auto const ter = verifyAggregatedBulletproof(bulletproof, commitments, contextHash);
!isTesSuccess(ter))
{
valid = false;
}
}
if (!valid)
return tecBAD_PROOF;
return tesSUCCESS;
}
TER
ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx)
{
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const account = ctx.tx[sfAccount];
auto const amount = ctx.tx[sfMPTAmount];
// ensure that issuance exists
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanConfidentialAmount))
return tecNO_PERMISSION;
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey);
// tx must include auditor ciphertext if the issuance has enabled
// auditing
if (requiresAuditor && !hasAuditor)
return tecNO_PERMISSION;
// if auditing is not supported then user should not upload auditor
// ciphertext
if (!requiresAuditor && hasAuditor)
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on
// the issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == account)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
if (!sleMptoken)
return tecOBJECT_NOT_FOUND;
if (!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleMptoken->isFieldPresent(sfHolderEncryptionKey))
{
return tecNO_PERMISSION;
}
// if the total circulating confidential balance is smaller than what the
// holder is trying to convert back, we know for sure this txn should
// fail
if ((*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) < amount)
{
return tecINSUFFICIENT_FUNDS;
}
// Check lock
MPTIssue const mptIssue(mptIssuanceID);
if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter))
return ter;
// Check auth
if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter))
return ter;
if (TER const res = verifyProofs(ctx.tx, sleIssuance, sleMptoken); !isTesSuccess(res))
return res;
return tesSUCCESS;
}
TER
ConfidentialMPTConvertBack::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
if (!sleMptoken)
return tecINTERNAL; // LCOV_EXCL_LINE
auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const amtToConvertBack = ctx_.tx[sfMPTAmount];
auto const amt = (*sleMptoken)[~sfMPTAmount].value_or(0);
// Converting back increases regular balance and decreases confidential
// outstanding. This is the inverse of Convert.
(*sleMptoken)[sfMPTAmount] = amt + amtToConvertBack;
(*sleIssuance)[sfConfidentialOutstandingAmount] =
(*sleIssuance)[sfConfidentialOutstandingAmount] - amtToConvertBack;
std::optional<Slice> const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
// homomorphically subtract holder's encrypted balance
{
auto res = homomorphicSubtract(
(*sleMptoken)[sfConfidentialBalanceSpending], ctx_.tx[sfHolderEncryptedAmount]);
if (!res)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*res);
}
// homomorphically subtract issuer's encrypted balance
{
auto res = homomorphicSubtract(
(*sleMptoken)[sfIssuerEncryptedBalance], ctx_.tx[sfIssuerEncryptedAmount]);
if (!res)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfIssuerEncryptedBalance] = std::move(*res);
}
if (auditorEc)
{
auto res = homomorphicSubtract(
(*sleMptoken)[sfAuditorEncryptedBalance], ctx_.tx[sfAuditorEncryptedAmount]);
if (!res)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfAuditorEncryptedBalance] = std::move(*res);
}
incrementConfidentialVersion(*sleMptoken);
view().update(sleIssuance);
view().update(sleMptoken);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -1,94 +0,0 @@
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h>
namespace xrpl {
NotTEC
ConfidentialMPTMergeInbox::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
// issuer cannot merge
if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
return temMALFORMED;
return tesSUCCESS;
}
TER
ConfidentialMPTMergeInbox::preclaim(PreclaimContext const& ctx)
{
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanConfidentialAmount))
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on the
// issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sleMptoken =
ctx.view.read(keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount]));
if (!sleMptoken)
return tecOBJECT_NOT_FOUND;
if (!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleMptoken->isFieldPresent(sfHolderEncryptionKey))
return tecNO_PERMISSION;
return tesSUCCESS;
}
TER
ConfidentialMPTMergeInbox::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
if (!sleMptoken)
return tecINTERNAL; // LCOV_EXCL_LINE
// sanity check
if (!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
!sleMptoken->isFieldPresent(sfHolderEncryptionKey))
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
// Merge inbox into spending: spending = spending + inbox
// This allows holder to use received funds. Without merging, incoming
// transfers sit in inbox and cannot be spent or converted back.
auto sum = homomorphicAdd(
(*sleMptoken)[sfConfidentialBalanceSpending], (*sleMptoken)[sfConfidentialBalanceInbox]);
if (!sum)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*sum);
// Reset inbox to encrypted zero. Must use canonical zero encryption
// (deterministic ciphertext) so the ledger state is reproducible.
auto zeroEncryption =
encryptCanonicalZeroAmount((*sleMptoken)[sfHolderEncryptionKey], account_, mptIssuanceID);
if (!zeroEncryption)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfConfidentialBalanceInbox] = std::move(*zeroEncryption);
incrementConfidentialVersion(*sleMptoken);
view().update(sleMptoken);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -1,407 +0,0 @@
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/token/ConfidentialMPTSend.h>
namespace xrpl {
NotTEC
ConfidentialMPTSend::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
auto const account = ctx.tx[sfAccount];
auto const issuer = MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer();
// ConfidentialMPTSend only allows holder to holder, holder to second account,
// and second account to holder transfers. So issuer cannot be the sender.
if (account == issuer)
return temMALFORMED;
// Can not send to self
if (account == ctx.tx[sfDestination])
return temMALFORMED;
// Check the length of the encrypted amounts
if (ctx.tx[sfSenderEncryptedAmount].length() != ecGamalEncryptedTotalLength ||
ctx.tx[sfDestinationEncryptedAmount].length() != ecGamalEncryptedTotalLength ||
ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
if (hasAuditor && ctx.tx[sfAuditorEncryptedAmount].length() != ecGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
// Check the length of the ZKProof
auto const recipientCount = getConfidentialRecipientCount(hasAuditor);
auto const sizeEquality = secp256k1_mpt_proof_equality_shared_r_size(recipientCount);
auto const sizePedersenLinkage = 2 * ecPedersenProofLength;
if (ctx.tx[sfZKProof].length() !=
sizeEquality + sizePedersenLinkage + ecDoubleBulletproofLength)
return temMALFORMED;
// Check the Pedersen commitments are valid
if (!isValidCompressedECPoint(ctx.tx[sfBalanceCommitment]) ||
!isValidCompressedECPoint(ctx.tx[sfAmountCommitment]))
return temMALFORMED;
// Check the encrypted amount formats, this is more expensive so put it at
// the end
if (!isValidCiphertext(ctx.tx[sfSenderEncryptedAmount]) ||
!isValidCiphertext(ctx.tx[sfDestinationEncryptedAmount]) ||
!isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount]))
return temBAD_CIPHERTEXT;
if (hasAuditor && !isValidCiphertext(ctx.tx[sfAuditorEncryptedAmount]))
return temBAD_CIPHERTEXT;
if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
return err;
return tesSUCCESS;
}
TER
verifySendProofs(
PreclaimContext const& ctx,
std::shared_ptr<SLE const> const& sleSenderMPToken,
std::shared_ptr<SLE const> const& sleDestinationMPToken,
std::shared_ptr<SLE const> const& sleIssuance)
{
// Sanity check
if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
auto const recipientCount = getConfidentialRecipientCount(hasAuditor);
auto const proof = ctx.tx[sfZKProof];
size_t remainingLength = proof.size();
size_t currentOffset = 0;
// Extract equality proof
auto const sizeEquality = secp256k1_mpt_proof_equality_shared_r_size(recipientCount);
if (remainingLength < sizeEquality)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const equalityProof = proof.substr(currentOffset, sizeEquality);
currentOffset += sizeEquality;
remainingLength -= sizeEquality;
// Extract Pedersen linkage proof for amount commitment
if (remainingLength < ecPedersenProofLength)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const amountLinkageProof = proof.substr(currentOffset, ecPedersenProofLength);
currentOffset += ecPedersenProofLength;
remainingLength -= ecPedersenProofLength;
// Extract Pedersen linkage proof for balance commitment
if (remainingLength < ecPedersenProofLength)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const balanceLinkageProof = proof.substr(currentOffset, ecPedersenProofLength);
currentOffset += ecPedersenProofLength;
remainingLength -= ecPedersenProofLength;
// Extract range proof
if (remainingLength < ecDoubleBulletproofLength)
return tecINTERNAL;
auto const rangeProof = proof.substr(currentOffset, ecDoubleBulletproofLength);
currentOffset += ecDoubleBulletproofLength;
remainingLength -= ecDoubleBulletproofLength;
if (remainingLength != 0)
return tecINTERNAL; // LCOV_EXCL_LINE
// Prepare recipient list
std::vector<ConfidentialRecipient> recipients;
recipients.reserve(recipientCount);
recipients.push_back(
{(*sleSenderMPToken)[sfHolderEncryptionKey], ctx.tx[sfSenderEncryptedAmount]});
recipients.push_back(
{(*sleDestinationMPToken)[sfHolderEncryptionKey], ctx.tx[sfDestinationEncryptedAmount]});
recipients.push_back({(*sleIssuance)[sfIssuerEncryptionKey], ctx.tx[sfIssuerEncryptedAmount]});
if (hasAuditor)
{
recipients.push_back(
{(*sleIssuance)[sfAuditorEncryptionKey], ctx.tx[sfAuditorEncryptedAmount]});
}
// Prepare the context hash
auto const contextHash = getSendContextHash(
ctx.tx[sfAccount],
ctx.tx[sfMPTokenIssuanceID],
ctx.tx.getSeqProxy().value(),
ctx.tx[sfDestination],
(*sleSenderMPToken)[~sfConfidentialBalanceVersion].value_or(0));
// Use a boolean flag to track validity instead of returning early on failure to prevent leaking
// information about which proof failed through timing differences
bool valid = true;
// Verify the multi-ciphertext equality proof
if (auto const ter = verifyMultiCiphertextEqualityProof(
equalityProof, recipients, recipientCount, contextHash);
!isTesSuccess(ter))
{
valid = false;
}
// Verify amount linkage
if (auto const ter = verifyAmountPcmLinkage(
amountLinkageProof,
ctx.tx[sfSenderEncryptedAmount],
(*sleSenderMPToken)[sfHolderEncryptionKey],
ctx.tx[sfAmountCommitment],
contextHash);
!isTesSuccess(ter))
{
valid = false;
}
// Verify balance linkage
if (auto const ter = verifyBalancePcmLinkage(
balanceLinkageProof,
(*sleSenderMPToken)[sfConfidentialBalanceSpending],
(*sleSenderMPToken)[sfHolderEncryptionKey],
ctx.tx[sfBalanceCommitment],
contextHash);
!isTesSuccess(ter))
{
valid = false;
}
// Verify Range Proof
{
Buffer pcRem;
// Derive PC_rem = PC_balance - PC_amount
if (auto const ter = computeSendRemainder(
ctx.tx[sfBalanceCommitment], ctx.tx[sfAmountCommitment], pcRem);
!isTesSuccess(ter))
{
valid = false;
}
else
{
// Aggregated commitments: [PC_amount, PC_rem]
// Prove that both the transfer amount and the remaining balance are in range
std::vector<Slice> commitments;
commitments.push_back(ctx.tx[sfAmountCommitment]);
commitments.push_back(Slice{pcRem.data(), pcRem.size()});
if (auto const ter = verifyAggregatedBulletproof(rangeProof, commitments, contextHash);
!isTesSuccess(ter))
{
valid = false;
}
}
}
if (!valid)
{
JLOG(ctx.j.trace()) << "ConfidentialMPTSend: One or more cryptographic proofs failed.";
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
ConfidentialMPTSend::preclaim(PreclaimContext const& ctx)
{
// Check if sender account exists
auto const account = ctx.tx[sfAccount];
if (!ctx.view.exists(keylet::account(account)))
return terNO_ACCOUNT;
// Check if destination account exists
auto const destination = ctx.tx[sfDestination];
if (!ctx.view.exists(keylet::account(destination)))
return tecNO_TARGET;
// Check if MPT issuance exists
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// Check if the issuance allows transfer
if (!sleIssuance->isFlag(lsfMPTCanTransfer))
return tecNO_AUTH;
// Check if issuance allows confidential transfer
if (!sleIssuance->isFlag(lsfMPTCanConfidentialAmount))
return tecNO_PERMISSION;
// Check if issuance has issuer ElGamal public key
if (!sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
return tecNO_PERMISSION;
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey);
// Tx must include auditor ciphertext if the issuance has enabled
// auditing, and must not include it if auditing is not enabled
if (requiresAuditor != hasAuditor)
return tecNO_PERMISSION;
// Sanity check: issuer isn't the sender
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
return tefINTERNAL; // LCOV_EXCL_LINE
// Check sender's MPToken existence
auto const sleSenderMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
if (!sleSenderMPToken)
return tecOBJECT_NOT_FOUND;
// Check sender's MPToken has necessary fields for confidential send
if (!sleSenderMPToken->isFieldPresent(sfHolderEncryptionKey) ||
!sleSenderMPToken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleSenderMPToken->isFieldPresent(sfIssuerEncryptedBalance))
return tecNO_PERMISSION;
// Sanity check: MPToken's auditor field must be present if auditing is
// enabled
if (requiresAuditor && !sleSenderMPToken->isFieldPresent(sfAuditorEncryptedBalance))
return tefINTERNAL;
// Check destination's MPToken existence
auto const sleDestinationMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, destination));
if (!sleDestinationMPToken)
return tecOBJECT_NOT_FOUND;
// Check destination's MPToken has necessary fields for confidential send
if (!sleDestinationMPToken->isFieldPresent(sfHolderEncryptionKey) ||
!sleDestinationMPToken->isFieldPresent(sfConfidentialBalanceInbox) ||
!sleDestinationMPToken->isFieldPresent(sfIssuerEncryptedBalance))
return tecNO_PERMISSION;
// Check lock
MPTIssue const mptIssue(mptIssuanceID);
if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter))
return ter;
if (auto const ter = checkFrozen(ctx.view, destination, mptIssue); !isTesSuccess(ter))
return ter;
// Check auth
if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter))
return ter;
if (auto const ter = requireAuth(ctx.view, mptIssue, destination); !isTesSuccess(ter))
return ter;
if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j);
!isTesSuccess(err))
return err;
return verifySendProofs(ctx, sleSenderMPToken, sleDestinationMPToken, sleIssuance);
}
TER
ConfidentialMPTSend::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto const destination = ctx_.tx[sfDestination];
auto sleSenderMPToken = view().peek(keylet::mptoken(mptIssuanceID, account_));
auto sleDestinationMPToken = view().peek(keylet::mptoken(mptIssuanceID, destination));
auto sleDestAcct = view().peek(keylet::account(destination));
if (!sleSenderMPToken || !sleDestinationMPToken || !sleDestAcct)
return tecINTERNAL; // LCOV_EXCL_LINE
if (auto err = verifyDepositPreauth(
ctx_.tx, ctx_.view(), account_, destination, sleDestAcct, ctx_.journal);
!isTesSuccess(err))
return err;
Slice const senderEc = ctx_.tx[sfSenderEncryptedAmount];
Slice const destEc = ctx_.tx[sfDestinationEncryptedAmount];
Slice const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
// Subtract from sender's spending balance
{
Slice const curSpending = (*sleSenderMPToken)[sfConfidentialBalanceSpending];
auto newSpending = homomorphicSubtract(curSpending, senderEc);
if (!newSpending)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleSenderMPToken)[sfConfidentialBalanceSpending] = std::move(*newSpending);
}
// Subtract from issuer's balance
{
Slice const curIssuerEnc = (*sleSenderMPToken)[sfIssuerEncryptedBalance];
auto newIssuerEnc = homomorphicSubtract(curIssuerEnc, issuerEc);
if (!newIssuerEnc)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleSenderMPToken)[sfIssuerEncryptedBalance] = std::move(*newIssuerEnc);
}
// Subtract from auditor's balance if present
if (auditorEc)
{
Slice const curAuditorEnc = (*sleSenderMPToken)[sfAuditorEncryptedBalance];
auto newAuditorEnc = homomorphicSubtract(curAuditorEnc, *auditorEc);
if (!newAuditorEnc)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleSenderMPToken)[sfAuditorEncryptedBalance] = std::move(*newAuditorEnc);
}
// Add to destination's inbox balance
{
Slice const curInbox = (*sleDestinationMPToken)[sfConfidentialBalanceInbox];
auto newInbox = homomorphicAdd(curInbox, destEc);
if (!newInbox)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleDestinationMPToken)[sfConfidentialBalanceInbox] = std::move(*newInbox);
}
// Add to issuer's balance
{
Slice const curIssuerEnc = (*sleDestinationMPToken)[sfIssuerEncryptedBalance];
auto newIssuerEnc = homomorphicAdd(curIssuerEnc, issuerEc);
if (!newIssuerEnc)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleDestinationMPToken)[sfIssuerEncryptedBalance] = std::move(*newIssuerEnc);
}
// Add to auditor's balance if present
if (auditorEc)
{
Slice const curAuditorEnc = (*sleDestinationMPToken)[sfAuditorEncryptedBalance];
auto newAuditorEnc = homomorphicAdd(curAuditorEnc, *auditorEc);
if (!newAuditorEnc)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleDestinationMPToken)[sfAuditorEncryptedBalance] = std::move(*newAuditorEnc);
}
// increment sender version only; receiver version is not modified by incoming sends
incrementConfidentialVersion(*sleSenderMPToken);
view().update(sleSenderMPToken);
view().update(sleDestinationMPToken);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -72,25 +72,6 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
if (ctx.view.rules().enabled(featureSingleAssetVault) && sleMpt->isFlag(lsfMPTLocked))
return tecNO_PERMISSION;
if (ctx.view.rules().enabled(featureConfidentialTransfer))
{
auto const sleMptIssuance =
ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
// if there still existing encrypted balances of MPT in
// circulation
if (sleMptIssuance &&
(*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) != 0)
{
// this MPT still has encrypted balance, since we don't know
// if it's non-zero or not, we won't allow deletion of
// MPToken
if (sleMpt->isFieldPresent(sfConfidentialBalanceInbox) ||
sleMpt->isFieldPresent(sfConfidentialBalanceSpending))
return tecHAS_OBLIGATIONS;
}
}
return tesSUCCESS;
}

View File

@@ -16,16 +16,6 @@ MPTokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx)
if (ctx.tx.isFieldPresent(sfMutableFlags) && !ctx.rules.enabled(featureDynamicMPT))
return false;
if (ctx.tx.isFlag(tfMPTCanConfidentialAmount) &&
!ctx.rules.enabled(featureConfidentialTransfer))
return false;
// can not set tmfMPTCannotMutateCanConfidentialAmount without featureConfidentialTransfer
auto const mutableFlags = ctx.tx[~sfMutableFlags];
if (mutableFlags && (*mutableFlags & tmfMPTCannotMutateCanConfidentialAmount) &&
!ctx.rules.enabled(featureConfidentialTransfer))
return false;
return true;
}

View File

@@ -1,4 +1,3 @@
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TxFlags.h>
@@ -28,23 +27,16 @@ struct MPTMutabilityFlags
{
std::uint32_t setFlag;
std::uint32_t clearFlag;
std::uint32_t mutabilityFlag;
std::uint32_t targetFlag;
bool isCannotMutate = false; // if true, cannot mutate by default.
std::uint32_t canMutateFlag;
};
static constexpr std::array<MPTMutabilityFlags, 7> mptMutabilityFlags = {
{{tmfMPTSetCanLock, tmfMPTClearCanLock, lsmfMPTCanMutateCanLock, lsfMPTCanLock},
{tmfMPTSetRequireAuth, tmfMPTClearRequireAuth, lsmfMPTCanMutateRequireAuth, lsfMPTRequireAuth},
{tmfMPTSetCanEscrow, tmfMPTClearCanEscrow, lsmfMPTCanMutateCanEscrow, lsfMPTCanEscrow},
{tmfMPTSetCanTrade, tmfMPTClearCanTrade, lsmfMPTCanMutateCanTrade, lsfMPTCanTrade},
{tmfMPTSetCanTransfer, tmfMPTClearCanTransfer, lsmfMPTCanMutateCanTransfer, lsfMPTCanTransfer},
{tmfMPTSetCanClawback, tmfMPTClearCanClawback, lsmfMPTCanMutateCanClawback, lsfMPTCanClawback},
{tmfMPTSetCanConfidentialAmount,
tmfMPTClearCanConfidentialAmount,
lsmfMPTCannotMutateCanConfidentialAmount,
lsfMPTCanConfidentialAmount,
true}}};
static constexpr std::array<MPTMutabilityFlags, 6> mptMutabilityFlags = {
{{tmfMPTSetCanLock, tmfMPTClearCanLock, lsmfMPTCanMutateCanLock},
{tmfMPTSetRequireAuth, tmfMPTClearRequireAuth, lsmfMPTCanMutateRequireAuth},
{tmfMPTSetCanEscrow, tmfMPTClearCanEscrow, lsmfMPTCanMutateCanEscrow},
{tmfMPTSetCanTrade, tmfMPTClearCanTrade, lsmfMPTCanMutateCanTrade},
{tmfMPTSetCanTransfer, tmfMPTClearCanTransfer, lsmfMPTCanMutateCanTransfer},
{tmfMPTSetCanClawback, tmfMPTClearCanClawback, lsmfMPTCanMutateCanClawback}}};
NotTEC
MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
@@ -53,28 +45,14 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
auto const metadata = ctx.tx[~sfMPTokenMetadata];
auto const transferFee = ctx.tx[~sfTransferFee];
auto const isMutate = mutableFlags || metadata || transferFee;
auto const hasIssuerElGamalKey = ctx.tx.isFieldPresent(sfIssuerEncryptionKey);
auto const hasAuditorElGamalKey = ctx.tx.isFieldPresent(sfAuditorEncryptionKey);
auto const txFlags = ctx.tx.getFlags();
auto const mutatePrivacy = mutableFlags &&
((*mutableFlags & (tmfMPTSetCanConfidentialAmount | tmfMPTClearCanConfidentialAmount)));
auto const hasDomain = ctx.tx.isFieldPresent(sfDomainID);
auto const hasHolder = ctx.tx.isFieldPresent(sfHolder);
if (isMutate && !ctx.rules.enabled(featureDynamicMPT))
return temDISABLED;
if ((hasIssuerElGamalKey || hasAuditorElGamalKey || mutatePrivacy) &&
!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
if (hasDomain && hasHolder)
if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder))
return temMALFORMED;
if (mutatePrivacy && hasHolder)
return temMALFORMED;
auto const txFlags = ctx.tx.getFlags();
// fails if both flags are set
if ((txFlags & tfMPTLock) && (txFlags & tfMPTUnlock))
@@ -85,12 +63,10 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
if (holderID && accountID == holderID)
return temMALFORMED;
if (ctx.rules.enabled(featureSingleAssetVault) || ctx.rules.enabled(featureDynamicMPT) ||
ctx.rules.enabled(featureConfidentialTransfer))
if (ctx.rules.enabled(featureSingleAssetVault) || ctx.rules.enabled(featureDynamicMPT))
{
// Is this transaction actually changing anything ?
if (txFlags == 0 && !hasDomain && !hasIssuerElGamalKey && !hasAuditorElGamalKey &&
!isMutate)
if (txFlags == 0 && !ctx.tx.isFieldPresent(sfDomainID) && !isMutate)
return temMALFORMED;
}
@@ -131,18 +107,6 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
}
}
if (hasHolder && (hasIssuerElGamalKey || hasAuditorElGamalKey))
return temMALFORMED;
if (hasAuditorElGamalKey && !hasIssuerElGamalKey)
return temMALFORMED;
if (hasIssuerElGamalKey && !isValidCompressedECPoint(ctx.tx[sfIssuerEncryptionKey]))
return temMALFORMED;
if (hasAuditorElGamalKey && !isValidCompressedECPoint(ctx.tx[sfAuditorEncryptionKey]))
return temMALFORMED;
return tesSUCCESS;
}
@@ -235,30 +199,16 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
return currentMutableFlags & mutableFlag;
};
auto const mutableFlags = ctx.tx[~sfMutableFlags];
if (mutableFlags)
if (auto const mutableFlags = ctx.tx[~sfMutableFlags])
{
if (std::any_of(
mptMutabilityFlags.begin(),
mptMutabilityFlags.end(),
[mutableFlags, &isMutableFlag](auto const& f) {
bool const canMutate = f.isCannotMutate ? isMutableFlag(f.mutabilityFlag)
: !isMutableFlag(f.mutabilityFlag);
return canMutate && (*mutableFlags & (f.setFlag | f.clearFlag));
return !isMutableFlag(f.canMutateFlag) &&
((*mutableFlags & (f.setFlag | f.clearFlag)));
}))
return tecNO_PERMISSION;
if ((*mutableFlags & tmfMPTSetCanConfidentialAmount) ||
(*mutableFlags & tmfMPTClearCanConfidentialAmount))
{
std::uint64_t const confidentialOA =
(*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0);
// If there's any confidential outstanding amount, disallow toggling
// the lsfMPTCanConfidentialAmount flag
if (confidentialOA > 0)
return tecNO_PERMISSION;
}
}
if (!isMutableFlag(lsmfMPTCanMutateMetadata) && ctx.tx.isFieldPresent(sfMPTokenMetadata))
@@ -277,40 +227,6 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
}
// cannot update issuer public key
if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
sleMptIssuance->isFieldPresent(sfIssuerEncryptionKey))
{
return tecNO_PERMISSION;
}
// cannot update auditor public key
if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
sleMptIssuance->isFieldPresent(sfAuditorEncryptionKey))
{
return tecNO_PERMISSION; // LCOV_EXCL_LINE
}
if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
!sleMptIssuance->isFlag(lsfMPTCanConfidentialAmount))
{
return tecNO_PERMISSION;
}
if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
!sleMptIssuance->isFlag(lsfMPTCanConfidentialAmount))
{
return tecNO_PERMISSION;
}
// cannot upload key if there's circulating supply of COA
if ((ctx.tx.isFieldPresent(sfIssuerEncryptionKey) ||
ctx.tx.isFieldPresent(sfAuditorEncryptionKey)) &&
sleMptIssuance->isFieldPresent(sfConfidentialOutstandingAmount))
{
return tecNO_PERMISSION; // LCOV_EXCL_LINE
}
return tesSUCCESS;
}
@@ -344,9 +260,9 @@ MPTokenIssuanceSet::doApply()
for (auto const& f : mptMutabilityFlags)
{
if (mutableFlags & f.setFlag)
flagsOut |= f.targetFlag;
flagsOut |= f.canMutateFlag;
else if (mutableFlags & f.clearFlag)
flagsOut &= ~f.targetFlag;
flagsOut &= ~f.canMutateFlag;
}
if (mutableFlags & tmfMPTClearCanTransfer)
@@ -398,26 +314,6 @@ MPTokenIssuanceSet::doApply()
}
}
if (auto const pubKey = ctx_.tx[~sfIssuerEncryptionKey])
{
// This is enforced in preflight.
XRPL_ASSERT(
sle->getType() == ltMPTOKEN_ISSUANCE,
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
sle->setFieldVL(sfIssuerEncryptionKey, *pubKey);
}
if (auto const pubKey = ctx_.tx[~sfAuditorEncryptionKey])
{
// This is enforced in preflight.
XRPL_ASSERT(
sle->getType() == ltMPTOKEN_ISSUANCE,
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
sle->setFieldVL(sfAuditorEncryptionKey, *pubKey);
}
view().update(sle);
return tesSUCCESS;

View File

@@ -2342,7 +2342,7 @@ private:
// The vote is not added to the slots
ammAlice.vote(carol, 1'000);
auto const info = ammAlice.ammRpcInfo()[jss::amm][jss::vote_slots];
for (std::uint16_t i = 0; i < info.size(); ++i)
for (std::uint32_t i = 0; i < info.size(); ++i)
BEAST_EXPECT(info[i][jss::account] != carol.human());
// But the slots are refreshed and the fee is changed
BEAST_EXPECT(ammAlice.expectTradingFee(82));

Some files were not shown because too many files have changed in this diff Show More