Merge branch 'develop' into refactor/consteval-specs-shared-leaf-util

This commit is contained in:
Alex Kremer
2026-08-28 16:33:44 +01:00
committed by GitHub
10 changed files with 247 additions and 14 deletions

View File

@@ -14,6 +14,18 @@ concurrency:
cancel-in-progress: true
jobs:
release-info:
name: Determine release info
runs-on: ubuntu-latest
outputs:
channel: ${{ steps.release_info.outputs.channel }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- id: release_info
uses: XRPLF/actions/release-info@7cc0e4a8d9d0b838f92c48d312856b190341bbba
build-and-test:
name: Build and Test
@@ -60,6 +72,16 @@ jobs:
targets: package
analyze_build_time: false
publish_package:
name: Publish debian package
needs: [release-info, package]
uses: ./.github/workflows/reusable-publish-package.yml
with:
channel: ${{ needs.release-info.outputs.channel }}
secrets:
nexus_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
nexus_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
release:
needs: [build-and-test, package]
uses: ./.github/workflows/reusable-release.yml

View File

@@ -0,0 +1,61 @@
# See docs/install-clio.md, "Publishing".
name: Reusable publish package
on:
workflow_call:
inputs:
channel:
description: "Release channel to publish to, selecting the deb-<channel> repository"
required: true
type: string
secrets:
nexus_username:
description: Username of a Nexus account with write access to the repositories
required: false
nexus_password:
description: Password or token for that Nexus account
required: false
defaults:
run:
shell: bash
env:
PACKAGE_DIR: packages
jobs:
publish:
name: Publish debian package
runs-on: heavy
container:
image: ghcr.io/xrplf/xrpld/packaging-debian:sha-b6a8995
permissions:
contents: read
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: ${{ env.PACKAGE_DIR }}
pattern: clio_deb_package_*
# dpkg-deb refuses to read a version CPack should not have produced.
- name: Verify packages
run: |
shopt -s globstar nullglob
for package in "${PACKAGE_DIR}"/**/*.deb; do
dpkg-deb --field "${package}" Package Version Architecture
done
- name: Publish packages
env:
CHANNEL: ${{ inputs.channel }}
NEXUS_USERNAME: ${{ secrets.nexus_username }}
NEXUS_PASSWORD: ${{ secrets.nexus_password }}
DRY_RUN_OPTION: ${{ (github.event_name == 'pull_request' || github.repository != 'XRPLF/clio') && '--dry-run' || '' }}
run: |
publish_pkg.py \
--channel "${CHANNEL}" \
--package-dir "${PACKAGE_DIR}" \
${DRY_RUN_OPTION}

View File

@@ -32,7 +32,8 @@ Below are some useful docs to learn more about Clio.
**For Operators**:
- [How to configure Clio and rippled](./docs/configure-clio.md)
- [How to install Clio](./docs/install-clio.md)
- [How to configure Clio and xrpld](./docs/configure-clio.md)
- [How to run Clio](./docs/run-clio.md)
- [Troubleshooting guide](./docs/trouble_shooting.md)

View File

@@ -54,8 +54,16 @@ else()
string(SUBSTRING ${GIT_COMMIT_HASH} 0 7 GIT_COMMIT_HASH_SHORT)
# Debian package versions may only contain alphanumerics and '.+~-'
string(
REGEX REPLACE "[^a-zA-Z0-9.+~-]"
"."
SANITIZED_BUILD_BRANCH
"${GIT_BUILD_BRANCH}"
)
set(CLIO_VERSION
"${BUILD_DATE}-${GIT_BUILD_BRANCH}-${GIT_COMMIT_HASH_SHORT}"
"${BUILD_DATE}-${SANITIZED_BUILD_BRANCH}-${GIT_COMMIT_HASH_SHORT}"
)
set(DOC_CLIO_VERSION "develop")
endif()

View File

@@ -9,4 +9,4 @@ set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA ${CMAKE_SOURCE_DIR}/cmake/pkg/postinst)
# We must replace "-" with "~" otherwise dpkg will sort "X.Y.Z-b1" as greater than "X.Y.Z"
string(REPLACE "-" "~" git "${CPACK_PACKAGE_VERSION}")
string(REPLACE "-" "~" CPACK_DEBIAN_PACKAGE_VERSION "${CPACK_PACKAGE_VERSION}")

35
docs/install-clio.md Normal file
View File

@@ -0,0 +1,35 @@
# How to install Clio
Clio is published as a Debian package for 64-bit x86 Linux, in the XRPL Foundation repositories at <https://packages.xrplf.org>. To build from source instead, see [How to build Clio](./build-clio.md).
## Install
Clio publishes to the same repositories as [`xrpld`](https://github.com/XRPLF/rippled), so configure APT by following [its instructions](https://github.com/XRPLF/rippled/blob/develop/docs/install.md#with-the-apt-package-manager), which also document the [release channels](https://github.com/XRPLF/rippled/blob/develop/docs/install.md#release-channels), and install `clio` in place of `xrpld`:
```bash
sudo apt -y install clio
```
## The clio service
The package installs `/opt/clio/bin/clio_server` (symlinked into `/usr/bin`), a config at `/opt/clio/etc/config.json`, a log directory at `/var/log/clio`, and a systemd unit, all owned by the `clio` system user it creates.
The unit is not enabled, as Clio needs a configured database and `xrpld` node before it can start. Edit the config — see [How to configure Clio and xrpld](./configure-clio.md) — then:
```bash
sudo systemctl enable --now clio
```
Upgrades do not restart a running server; `systemctl restart clio` picks up the new binary.
## Publishing
Only tags are published. CPack builds the package in the `package` job of [`release.yml`](../.github/workflows/release.yml), and [`reusable-publish-package.yml`](../.github/workflows/reusable-publish-package.yml) uploads it with `publish_pkg.py` from the [`xrpld` packaging image](https://github.com/XRPLF/rippled/blob/develop/package/README.md#publishing-packages), so that Clio and `xrpld` agree on what a channel means. [`XRPLF/actions/release-info`](https://github.com/XRPLF/actions/blob/main/release-info/action.yml) picks the channel:
| Tag | Package version | Channel |
| ----------- | --------------- | -------- |
| `X.Y.Z` | `X.Y.Z` | `stable` |
| `X.Y.Z-rcN` | `X.Y.Z~rcN` | `rc` |
| `X.Y.Z-bN` | `X.Y.Z~bN` | `beta` |
[`cmake/pkg/deb.cmake`](../cmake/pkg/deb.cmake) converts the `-` to `~`, which Debian sorts below everything, so `X.Y.Z~rc1` ranks below the `X.Y.Z` it precedes.

View File

@@ -182,7 +182,7 @@ tag_invoke(
ASSERT(!name.empty(), "Field name is empty after stripping 'ID'");
}
// ValidPseudoAccounts invariant guarantees that only one field can be set
jv.as_object()[JS(pseudo_account)].as_object()[JS(type)] = name;
jv.as_object()[JS(pseudo_account)] = boost::json::object{{JS(type), name}};
break;
}
}

View File

@@ -40,13 +40,13 @@ struct ClusterBackendTest : util::prometheus::WithPrometheus, MockBackendTestStr
testing::StrictMock<
testing::MockFunction<void(ClioNode::CUuid, std::shared_ptr<Backend::ClusterData const>)>>
callbackMock;
std::binary_semaphore semaphore{0};
std::counting_semaphore<> semaphore{0};
class SemaphoreReleaseGuard {
std::binary_semaphore& semaphore_;
std::counting_semaphore<>& semaphore_;
public:
SemaphoreReleaseGuard(std::binary_semaphore& s) : semaphore_(s)
SemaphoreReleaseGuard(std::counting_semaphore<>& s) : semaphore_(s)
{
}
~SemaphoreReleaseGuard()
@@ -72,7 +72,7 @@ TEST_F(ClusterBackendTest, SubscribeToNewState)
EXPECT_CALL(*backend_, fetchClioNodesData)
.Times(testing::AtLeast(1))
.WillRepeatedly(testing::Return(BackendInterface::ClioNodesDataFetchResult{}));
EXPECT_CALL(*backend_, writeNodeMessage).Times(testing::AtLeast(1));
EXPECT_CALL(*backend_, writeNodeMessage).Times(testing::AnyNumber());
EXPECT_CALL(writerStateRef, isReadOnly)
.Times(testing::AtLeast(1))
.WillRepeatedly(testing::Return(true));
@@ -151,7 +151,7 @@ TEST_F(ClusterBackendTest, FetchClioNodesDataThrowsException)
EXPECT_CALL(*backend_, fetchClioNodesData)
.Times(testing::AtLeast(1))
.WillRepeatedly(testing::Throw(std::runtime_error("Database connection failed")));
EXPECT_CALL(*backend_, writeNodeMessage).Times(testing::AtLeast(1));
EXPECT_CALL(*backend_, writeNodeMessage).Times(testing::AnyNumber());
EXPECT_CALL(writerStateRef, isReadOnly)
.Times(testing::AtLeast(1))
.WillRepeatedly(testing::Return(true));
@@ -208,7 +208,7 @@ TEST_F(ClusterBackendTest, FetchClioNodesDataReturnsDataWithOtherNodes)
}
)
);
EXPECT_CALL(*backend_, writeNodeMessage).Times(testing::AtLeast(1));
EXPECT_CALL(*backend_, writeNodeMessage).Times(testing::AnyNumber());
EXPECT_CALL(writerStateRef, isReadOnly)
.Times(testing::AtLeast(1))
.WillRepeatedly(testing::Return(false));
@@ -286,7 +286,7 @@ TEST_F(ClusterBackendTest, FetchClioNodesDataReturnsOnlySelfData)
}
};
});
EXPECT_CALL(*backend_, writeNodeMessage).Times(testing::AtLeast(1));
EXPECT_CALL(*backend_, writeNodeMessage).Times(testing::AnyNumber());
EXPECT_CALL(writerStateRef, isReadOnly)
.Times(testing::AtLeast(1))
.WillRepeatedly(testing::Return(true));
@@ -342,7 +342,7 @@ TEST_F(ClusterBackendTest, FetchClioNodesDataReturnsInvalidJson)
}
)
);
EXPECT_CALL(*backend_, writeNodeMessage).Times(testing::AtLeast(1));
EXPECT_CALL(*backend_, writeNodeMessage).Times(testing::AnyNumber());
EXPECT_CALL(writerStateRef, isReadOnly)
.Times(testing::AtLeast(1))
.WillRepeatedly(testing::Return(true));
@@ -398,7 +398,7 @@ TEST_F(ClusterBackendTest, FetchClioNodesDataReturnsValidJsonButCannotConvertToC
}
)
);
EXPECT_CALL(*backend_, writeNodeMessage).Times(testing::AtLeast(1));
EXPECT_CALL(*backend_, writeNodeMessage).Times(testing::AnyNumber());
EXPECT_CALL(writerStateRef, isReadOnly)
.Times(testing::AtLeast(1))
.WillRepeatedly(testing::Return(true));
@@ -536,7 +536,7 @@ TEST_F(ClusterBackendTest, SubscribeToNewStateReflectsCacheIsCurrentlyLoading)
EXPECT_CALL(*backend_, fetchClioNodesData)
.Times(testing::AtLeast(1))
.WillRepeatedly(testing::Return(BackendInterface::ClioNodesDataFetchResult{}));
EXPECT_CALL(*backend_, writeNodeMessage).Times(testing::AtLeast(1));
EXPECT_CALL(*backend_, writeNodeMessage).Times(testing::AnyNumber());
EXPECT_CALL(writerStateRef, isReadOnly)
.Times(testing::AtLeast(1))
.WillRepeatedly(testing::Return(true));

View File

@@ -438,6 +438,80 @@ TEST_F(RPCAccountInfoHandlerTest, SignerListsTrueV2)
});
}
TEST_F(RPCAccountInfoHandlerTest, PseudoAccountReportsType)
{
auto const expectedOutput = fmt::format(
R"JSON({{
"account_data": {{
"Account": "{}",
"AMMID": "{}",
"Balance": "200",
"Flags": 0,
"LedgerEntryType": "AccountRoot",
"OwnerCount": 2,
"PreviousTxnID": "{}",
"PreviousTxnLgrSeq": 2,
"Sequence": 2,
"TransferRate": 0,
"index": "13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8"
}},
"account_flags": {{
"defaultRipple": false,
"depositAuth": false,
"disableMasterKey": false,
"disallowIncomingXRP": false,
"globalFreeze": false,
"noFreeze": false,
"passwordSpent": false,
"requireAuthorization": false,
"requireDestinationTag": false
}},
"pseudo_account": {{
"type": "AMM"
}},
"ledger_hash": "{}",
"ledger_index": 30,
"validated": true
}})JSON",
kAccount,
kIndex1,
kIndex1,
kLedgerHash
);
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto const account = getAccountIdWithString(kAccount);
auto const accountKk = xrpl::keylet::account(account).key;
auto const accountRoot =
createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2, 0, xrpl::uint256{kIndex1});
ON_CALL(*backend_, doFetchLedgerObject(accountKk, 30, _))
.WillByDefault(Return(accountRoot.getSerializer().peekData()));
EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(_, Amendments::DisallowIncoming, _))
.WillOnce(Return(false));
EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(_, Amendments::Clawback, _))
.WillOnce(Return(false));
EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(_, Amendments::TokenEscrow, _))
.WillOnce(Return(false));
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(1);
static auto const kInput = boost::json::parse(
fmt::format(
R"JSON({{
"account": "{}"
}})JSON",
kAccount
)
);
auto const handler = AnyHandler{AccountInfoHandler{backend_, mockAmendmentCenterPtr_}};
runSpawn([&](auto yield) {
auto const output = handler.process(kInput, Context{.yield = yield, .apiVersion = 2});
ASSERT_TRUE(output);
EXPECT_EQ(*output.result, boost::json::parse(expectedOutput));
});
}
TEST_F(RPCAccountInfoHandlerTest, SignerListsTrueV1)
{
auto const expectedOutput = fmt::format(

View File

@@ -2026,6 +2026,38 @@ generateTestValuesForParametersTest()
.expectedError = "malformedRequest",
.expectedErrorMessage = "Malformed request."
},
ParamTestCaseBundle{
.testName = "CredentialCredentialTypeNotHex",
.testJson = fmt::format(
R"JSON({{
"credential": {{
"subject": "{}",
"issuer": "{}",
"credential_type": "hello world"
}}
}})JSON",
kAccount,
kAccount2
),
.expectedError = "malformedAuthorizedCredentials",
.expectedErrorMessage = "credential_type NotHexString"
},
ParamTestCaseBundle{
.testName = "CredentialCredentialTypeEmpty",
.testJson = fmt::format(
R"JSON({{
"credential": {{
"subject": "{}",
"issuer": "{}",
"credential_type": ""
}}
}})JSON",
kAccount,
kAccount2
),
.expectedError = "malformedAuthorizedCredentials",
.expectedErrorMessage = "credential_type is empty"
},
ParamTestCaseBundle{
.testName = "InvalidMPTokenAccount",
.testJson = fmt::format(