Merge remote-tracking branch 'upstream/develop' into refactor/consteval-specs-dual-path

This commit is contained in:
Alex Kremer
2026-09-01 17:44:24 +01:00
13 changed files with 253 additions and 20 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

@@ -100,14 +100,14 @@ repos:
files: \.(h|hpp)$
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: f4d7745e17a28aad7eed2f4874ca8d1568c11c4c # frozen: v22.1.8
rev: 34b241d706d24e4ccb69e38f9f0fa190b5f486ec # frozen: v23.1.0
hooks:
- id: clang-format
args: [--style=file]
types: [c++]
- repo: https://github.com/BlankSpruce/gersemi-pre-commit
rev: b9d6d13dc9b753c5ff24ed5f8470671189c3fcf0 # frozen: 0.28.0
rev: c1c636fef559eeef0dae280afcd44420f6126e63 # frozen: 0.28.1
hooks:
- id: gersemi

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

@@ -156,7 +156,7 @@ tag_invoke(
{"disallowIncomingCheck", xrpl::lsfDisallowIncomingCheck},
{"disallowIncomingPayChan", xrpl::lsfDisallowIncomingPayChan},
{"disallowIncomingTrustline", xrpl::lsfDisallowIncomingTrustline},
};
};
lsFlags.insert(lsFlags.end(), disallowIncomingFlags.begin(), disallowIncomingFlags.end());
}
@@ -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

@@ -110,7 +110,7 @@ std::
R"(0x5e48b5a0b005398d0ee0bb53f8af2b5e56a9854d3c4e082e2fcf855597bf826d,5619393,0x110061220000000024000b82b125000b82b72d0000000155b0b6cfd9a072bb5623b747e94ae3a8467db6ac48425193e0654c528ca8d2a5bb624000000002160e9881142aa9fe6eb2a76050206ada5c99f5adbc77fc3211)",
R"(0x2f7999f58d61141f63482028c4735da4bdb31ff2c6cb874042816a26ffc4f9a3,5619393,0x110072220021000025004886cd370000000000000000380000000000000000554cffbe7e5472934ec1204959f9c1816fc471c5c33ce1a3f94fce39d01b904e0b6280000000000000000000000000000000000000005553440000000000000000000000000000000000000000000000000166d6c38d7ea4c680000000000000000000000000005553440000000000159aff3c197cf41ef37ae60ebe81ce6c89aa902b6780000000000000000000000000000000000000005553440000000000d8c7757a2b0c8def0466584581b2764be1aa2ce2)",
R"(0x231c2687efb2778a40613b947cfa878a62f05980564ef0b618af25183cedc97c,5619393,0x11006422000000002500197b42554e3104a116c3e430bf660101e95d123632b79895b144ca6c588acb5f582909d158231c2687efb2778a40613b947cfa878a62f05980564ef0b618af25183cedc97c82142c8211b22ae69fe6feef60ae4ededb815911f27b011320df2b95081e054472d9505cc542745786a237cb9606691e70d5abd15db9d3ce58)",
};
};
// The raw data from ledgers table in the database
std::
@@ -167,4 +167,4 @@ std::
R"(5619440,0x0055bef0016344ab372c8d3081444a555110bf03fd6aff2eefc1cbf4d1657ea638259a339c415916a20b340d00000000000000000000000000000000000000000000000000000000000000005a3e310fec8e52b333e96ab40b4ac5036d238e4c7fd4053570f7960ba326ba482ebe287c2ebe287d0a00b4292e72717a68eb72ac8f6df5b114309a31c62beb668e28b2e4c3a0470e0de6)",
R"(5619441,0x0055bef1016344ab372c8d30b4292e72717a68eb72ac8f6df5b114309a31c62beb668e28b2e4c3a0470e0de600000000000000000000000000000000000000000000000000000000000000006d1db0156c8129b4e22869e0f6730febb60cc50a828b4a62122d3c71a837c9e92ebe287d2ebe287e0a0031fab27d3a5a84ac592ef823efbae53ee6ceb688b973bd88921f5745d32f227a)",
R"(5619442,0x0055bef2016344ab372c8d3031fab27d3a5a84ac592ef823efbae53ee6ceb688b973bd88921f5745d32f227a0000000000000000000000000000000000000000000000000000000000000000bddfdb6e2f3f7d0ae8568c7acf3e311783c491c2dc079e7a0191c75e9b9e10842ebe287e2ebe287f0a006c9407d47becf698460a8af15bbdbad92022182ec519e0f7cb0c0faa43f8bd0f)"
};
};

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

@@ -112,7 +112,7 @@ protected:
grpcSourceMock_,
subscriptionSourceMock_,
forwardingSourceMock_
};
};
};
TEST_F(SourceImplTest, run)

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(