Compare commits

..

1 Commits

Author SHA1 Message Date
TimothyBanks
03511bf0ec feat: Smart Contract codec poc 2026-07-10 12:00:14 -04:00
15 changed files with 679 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
#pragma once
#include <xrpl/basics/Expected.h>
#include <xrpl/tx/wasm/CodecEnvelope.h>
namespace xrpl::wasm {
// ─────────────────────────────────────────────────────────────────────────
// Public wasm codec API.
//
// This header is what host-function code includes. It intentionally contains
// NO codec machinery, NO registered-codec list, and NO concrete type
// registrations — only the two entry points. Their definitions are provided by
// explicit instantiation in src/libxrpl/tx/wasm/codecs/CodecRegistry.cpp, so
// including this header does not instantiate the (consteval) dispatch tables.
//
// A decode<T>/encode<T> for an unregistered T is a link error by design.
//
// To add a codec, see include/xrpl/tx/wasm/detail/codec_types.macro.
// ─────────────────────────────────────────────────────────────────────────
// Decode a value of the statically-known type T from a self-describing
// envelope. The envelope carries its own (type_code, version), so no revision
// context is needed here.
template <typename T>
[[nodiscard]] Expected<T, CodecError>
decode(CodecEnvelope const& env);
// Encode `value` toward a contract that declared codec revision
// `declaredRevision`. The wire version emitted is chosen by the revision policy
// (wireVersionFor); if the value cannot be represented in that version the
// result is NotRepresentable (the graceful-fail path).
template <typename T>
[[nodiscard]] Expected<CodecEnvelope, CodecError>
encode(CodecVersion declaredRevision, T const& value);
} // namespace xrpl::wasm

View File

@@ -0,0 +1,35 @@
#pragma once
#include <cstdint>
namespace xrpl::wasm {
using CodecTypeCode = std::uint16_t;
using CodecVersion = std::uint8_t;
using CodecLength = std::uint32_t;
enum class CodecError : std::uint8_t {
Ok = 0,
UnknownType = 1, // no type code registered for the type
UnsupportedVersion = 2, // The envelope's version has no registered codec
TypeMismatch = 3, // When decoding, the codec called does not match the type of the envelope
Truncated = 4, // payload size != fixed payload size
NotRepresentable = 5, // when encoding, the value doesn't fit within the requested version
TypeNotInRevision = 6, // There is no codec for the type in the requested version
UnknownCodecVersion = 7, // The codec version is not registered
};
// Defines the header of the binary payload for each parameter based through the host functions
struct CodecEnvelopeHeader {
CodecTypeCode type_code;
CodecVersion version;
CodecLength payload_size; // I would prefer a variable sized integer, will need to revisit this part.
};
// Defines the binary payload for each parameter based through the host functions
struct CodecEnvelope {
CodecEnvelopeHeader header;
std::uint8_t const* payload; // Unclear if this can be a pointer into the wasm payload or a vector holding a copy.
};
} // namespace xrpl::wasm

View File

@@ -0,0 +1,145 @@
#pragma once
#include <xrpl/basics/Expected.h>
#include <xrpl/tx/wasm/CodecEnvelope.h>
#include <xrpl/tx/wasm/detail/CodecTypeCode.h>
#include <xrpl/tx/wasm/detail/CodecTypeTrait.h>
#include <array>
#include <cstddef>
#include <tuple>
#include <type_traits>
#include <utility>
namespace xrpl::wasm::detail {
// Sentinel that lets the registry's X-macro expansion end with a trailing
// comma: CodecList<Trait0, Trait1, CodecEnd>. Its `void` CodecType never
// matches a real type, so every table builder simply skips it.
struct CodecEnd
{
using CodecType = void;
};
template <typename Trait, typename T>
inline constexpr bool traitIsV = std::is_same_v<typename Trait::CodecType, T>;
template <typename T>
using DecodeFn = Expected<T, CodecError> (*)(CodecEnvelope const&);
template <typename T>
using EncodeFn = Expected<CodecEnvelope, CodecError> (*)(T const&);
// Highest registered version of T within List. Unregistered T ⇒ build error.
template <typename T, typename List, std::size_t... I>
consteval CodecVersion
maxVersionImpl(std::index_sequence<I...>)
{
auto v = CodecVersion{};
auto found = false;
([&] {
using Trait = std::tuple_element_t<I, List>;
if constexpr (traitIsV<Trait, T>)
{
found = true;
v = std::max(v, Trait::kVersion);
}
}(), ...);
if (!found)
{
xrpl::Throw<std::runtime_error>("wasm codec: no codec registered for this type");
}
return v;
}
template <typename T, typename List>
consteval CodecVersion
maxVersion()
{
return maxVersionImpl<T, List>(
std::make_index_sequence<std::tuple_size_v<List>>{});
}
template <typename T, typename List, std::size_t... I>
consteval auto
makeDecoderTableImpl(std::index_sequence<I...>)
{
constexpr auto n = maxVersion<T, List>() + 1;
auto table = std::array<DecodeFn<T>, n>{};
([&] {
using Trait = std::tuple_element_t<I, List>;
if constexpr (traitIsV<Trait, T>)
{
table[Trait::kVersion] = &Trait::decode;
}
}(), ...);
for (const auto& entry : table)
{
if (!entry)
{
xrpl::Throw<std::runtime_error>("wasm codec: decoder versions must be contiguous from 0");
}
}
return table;
}
template <typename T, typename List>
inline constexpr auto decoderTableV = makeDecoderTableImpl<T, List>(
std::make_index_sequence<std::tuple_size_v<List>>{});
template <typename T, typename List, std::size_t... I>
consteval auto
makeEncoderTableImpl(std::index_sequence<I...>)
{
constexpr auto n = maxVersion<T, List>() + 1;
auto table = std::array<EncodeFn<T>, n>{};
([&] {
using Trait = std::tuple_element_t<I, List>;
if constexpr (traitIsV<Trait, T>)
{
table[Trait::kVersion] = &Trait::encode;
}
}(), ...);
for (const auto& entry : table)
{
if (!entry)
{
xrpl::Throw<std::runtime_error>("wasm codec: encoder versions must be contiguous from 0");
}
}
return table;
}
template <typename T, typename List>
inline constexpr auto encoderTableV = makeEncoderTableImpl<T, List>(
std::make_index_sequence<std::tuple_size_v<List>>{});
template <typename T, typename List>
Expected<T, CodecError>
decodeImpl(CodecEnvelope const& env)
{
if (env.header.type_code != codecTypeCodeV<T>)
{
return Unexpected{CodecError::TypeMismatch};
}
auto const& table = decoderTableV<T, List>;
if (env.header.version >= table.size())
{
return Unexpected{CodecError::UnsupportedVersion};
}
return table[env.header.version](env);
}
template <typename T, typename List>
Expected<CodecEnvelope, CodecError>
encodeAt(CodecVersion wireVersion, T const& value)
{
auto const& table = encoderTableV<T, List>;
if (wireVersion >= table.size())
{
return Unexpected{CodecError::UnsupportedVersion};
}
return table[wireVersion](value);
}
} // namespace xrpl::wasm::detail

View File

@@ -0,0 +1,12 @@
#pragma once
#include <tuple>
namespace xrpl::wasm::detail {
template <typename... Traits>
struct CodecList {
using TupleType = std::tuple<Traits...>;
};
} // namespace xrpl::wasm::detail

View File

@@ -0,0 +1,18 @@
#pragma once
#include <xrpl/basics/Expected.h>
#include <xrpl/tx/wasm/CodecEnvelope.h>
namespace xrpl::wasm::detail {
// Maps a contract's declared codec revision to the per-type wire version to
// emit for `code`. This is the revision/amendment POLICY layer, kept separate
// from both the generic dispatch machinery and the concrete value codecs.
//
// Defined in src/libxrpl/tx/wasm/codecs/CodecRevisions.cpp alongside the
// concrete CodecTrait<N> revision maps. (Amendment gating via Rules is a
// deliberate TODO there — see the .cpp.)
Expected<CodecVersion, CodecError>
wireVersionFor(CodecVersion declaredRevision, CodecTypeCode code);
} // namespace xrpl::wasm::detail

View File

@@ -0,0 +1,18 @@
#pragma once
#include <xrpl/tx/wasm/CodecEnvelope.h>
namespace xrpl::wasm::detail {
template <CodecVersion Version>
struct CodecTrait;
// template <>
// struct CodecTrait<0> {
// static constexpr auto kVersion = CodecVersion{0};
// static constexpr auto versions = std::to_array<std::pair<CodecTypeCode, CodecVersion>>({
// {codecTypeCodeV<STNumber>, 0},
// });
// };
} // namespace xrpl::wasm::detail

View File

@@ -0,0 +1,21 @@
#pragma once
#include <xrpl/tx/wasm/CodecEnvelope.h>
namespace xrpl::wasm::detail {
template <typename T>
struct CodecTypeCodeOf;
// Defines a type trait for registering a type code for each
// type in the codec.
template <typename T>
inline constexpr CodecTypeCode codecTypeCodeV = CodecTypeCodeOf<T>::kCodecTypeCode;
#define XRPL_WASM_CODEC_TYPE_CODE(CppType, Code) \
template <> \
struct CodecTypeCodeOf<CppType> { \
static constexpr CodecTypeCode kCodecTypeCode = Code; \
};
} // namespace xrpl::wasm::detail

View File

@@ -0,0 +1,27 @@
#pragma once
#include <xrpl/tx/wasm/CodecEnvelope.h>
#include <limits>
namespace xrpl::wasm::detail {
// Defines a type trait for registering a specific type code and version
// encoding/decoding implementation for each type in the codec.
template <CodecTypeCode TypeCode, CodecVersion Version>
struct CodecTypeTrait;
#define XRPL_WASM_CODEC_TYPE_TRAIT(CppType, Version, PayloadSize) \
template <> \
struct CodecTypeTrait<codecTypeCodeV<CppType>, Version> { \
using CodecType = CppType; \
static constexpr auto kCodecTypeCode = codecTypeCodeV<CppType>; \
static constexpr auto kVersion = Version; \
static constexpr auto kPayloadSize = PayloadSize; \
static Expected<CodecType, CodecError> decode(CodecEnvelope const& env); \
static Expected<CodecEnvelope, CodecError> encode(CodecType const& value); \
};
inline constexpr auto kVariablePayloadSize = std::numeric_limits<CodecLength>::max();
} // namespace xrpl::wasm::detail

View File

@@ -0,0 +1,48 @@
//
// codec_revisions.macro — the SINGLE registration point for codec
// REVISIONS (the "contract-declared codec revision -> per-type wire version"
// pinning). Included multiple times with different macro definitions, so NO
// include guard.
//
// A revision fixes, for every type, which wire version a contract that declared
// that revision receives. It is deliberately SEPARATE from the (type, version)
// registry: adding a codec version does not change any revision — bumping a
// revision is its own act, tied to the amendment that enables it.
//
// CODEC_REVISION_BEGIN(Revision)
// CODEC_REVISION_ENTRY(CppType, Version) // repeated, one per type
// CODEC_REVISION_END(Revision)
//
// HOW TO ADD A REVISION (e.g. revision 1 that emits the widened Number v1):
// 1. Add a block here:
// CODEC_REVISION_BEGIN(1)
// CODEC_REVISION_ENTRY(Number, 1) // widened
// CODEC_REVISION_ENTRY(AccountID, 0) // unchanged
// CODEC_REVISION_END(1)
// 2. Gate it behind its amendment in CodecRevisions.cpp (see the
// amendment-gating TODO in wireVersionFor).
// Both CodecTrait<1> and the wireVersionFor switch case regenerate from this.
//
#ifndef CODEC_REVISION_BEGIN
#define CODEC_REVISION_BEGIN(Revision)
#endif
#ifndef CODEC_REVISION_ENTRY
#define CODEC_REVISION_ENTRY(CppType, Version)
#endif
#ifndef CODEC_REVISION_END
#define CODEC_REVISION_END(Revision)
#endif
// clang-format off
CODEC_REVISION_BEGIN(0)
CODEC_REVISION_ENTRY(Number, 0)
CODEC_REVISION_ENTRY(AccountID, 0)
CODEC_REVISION_END(0)
// clang-format on
#undef CODEC_REVISION_BEGIN
#undef CODEC_REVISION_ENTRY
#undef CODEC_REVISION_END

View File

@@ -0,0 +1,58 @@
//
// codec_types.macro — the SINGLE registration point for wasm codecs.
//
// This file is #included multiple times with different macro definitions, so
// it has NO include guard. Every codec is declared here exactly once, on two
// axes, and everything else (the trait tuple, the dispatch tables, the
// explicit instantiations, the type-code map) is generated from it — so those
// can never drift apart.
//
// CODEC_TYPE(CppType, TypeCode)
// Once per C++ type. Assigns its stable 16-bit wire type code. Drives the
// type-code map and the per-type explicit instantiations of decode/encode.
//
// CODEC_VERSION(CppType, Version, PayloadSize)
// Once per (type, version). Declares a CodecTypeTrait specialization and
// adds it to the registered-codec tuple. Versions per type MUST be
// contiguous from 0 (enforced at compile time).
//
// HOW TO ADD A CODEC
//
// A) A new VERSION of an existing type (e.g. widen Number):
// 1. Add a line here: CODEC_VERSION(Number, 1, 20)
// 2. Define its bodies in the type's .cpp (codecs/NumberCodec.cpp):
// CodecTypeTrait<codecTypeCodeV<Number>, 1>::decode/encode
// 3. If a codec revision should emit it, add an entry in
// codec_revisions.macro.
// (Versions must stay contiguous from 0, or the build fails.)
//
// B) A brand-new TYPE (e.g. STAmount):
// 1. CODEC_TYPE(STAmount, 0x0003) and CODEC_VERSION(STAmount, 0, N) here.
// 2. Add its C++ header include to codecs/CodecTypes.h.
// 3. Create codecs/STAmountCodec.cpp with the decode/encode bodies.
// (GLOB_RECURSE picks up the new .cpp automatically — no CMake edit.)
// 4. Add a CODEC_REVISION_ENTRY(STAmount, 0) to each relevant revision
// block in codec_revisions.macro.
//
#ifndef CODEC_TYPE
#define CODEC_TYPE(CppType, TypeCode)
#endif
#ifndef CODEC_VERSION
#define CODEC_VERSION(CppType, Version, PayloadSize)
#endif
// clang-format off
// C++ type wire type code
CODEC_TYPE(Number, 0x0001)
CODEC_TYPE(AccountID, 0x0002)
// C++ type version payload bytes
CODEC_VERSION(Number, 0, 12) // int64 mantissa + int32 exponent
CODEC_VERSION(AccountID, 0, 20) // 20 raw bytes
// clang-format on
#undef CODEC_TYPE
#undef CODEC_VERSION

View File

@@ -0,0 +1,38 @@
#include "CodecTypes.h"
#include <cstring>
// ───────────────────────────────────────────────────────────────────────────
// Codec bodies for AccountID. One decode/encode pair per registered version.
// ───────────────────────────────────────────────────────────────────────────
namespace xrpl::wasm::detail {
// ── AccountID, version 0 : 20 raw bytes ─────────────────────────────────────
template <>
Expected<AccountID, CodecError>
CodecTypeTrait<codecTypeCodeV<AccountID>, 0>::decode(CodecEnvelope const& env)
{
if (env.header.payload_size != kPayloadSize)
{
return Unexpected{CodecError::Truncated};
}
auto id = AccountID{};
std::memcpy(id.data(), env.payload, id.size());
return id;
}
template <>
Expected<CodecEnvelope, CodecError>
CodecTypeTrait<codecTypeCodeV<AccountID>, 0>::encode(AccountID const& value)
{
// AccountID is fixed-width, so it always fits v0 (no NotRepresentable path).
// TODO(payload-ownership): payload bytes (value.data(), value.size()) must
// be owned by the wire layer; deferred per the current encode signature.
(void)value;
return CodecEnvelope{.header={.type_code=kCodecTypeCode, .version=kVersion, .payload_size=kPayloadSize}, .payload=nullptr};
}
} // namespace xrpl::wasm::detail

View File

@@ -0,0 +1,60 @@
#include <xrpl/tx/wasm/Codec.h>
#include <xrpl/tx/wasm/detail/CodecDispatch.h>
#include <xrpl/tx/wasm/detail/CodecList.h>
#include <xrpl/tx/wasm/detail/CodecRevisions.h>
#include "CodecTypes.h"
//
// The registry translation unit. This is the ONE place that:
// * assembles the complete registered-codec tuple from the .def, and
// * explicitly instantiates decode<T>/encode<T> for every registered type.
//
// Because the definitions live here (not in the public header), callers get the
// codec machinery out of their compiles, and an unregistered decode<T>/encode<T>
// fails at link time rather than with a template error.
//
namespace xrpl::wasm {
namespace {
// The single enumeration of every registered codec, built straight from the
// .def. The trailing detail::CodecEnd absorbs the final comma of the expansion.
using RegisteredCodecs = detail::CodecList<
#define CODEC_VERSION(CppType, Version, PayloadSize) \
detail::CodecTypeTrait<detail::codecTypeCodeV<CppType>, Version>,
#include <xrpl/tx/wasm/detail/codec_types.macro>
detail::CodecEnd>::TupleType;
} // namespace
template <typename T>
Expected<T, CodecError>
decode(CodecEnvelope const& env)
{
return detail::decodeImpl<T, RegisteredCodecs>(env);
}
template <typename T>
Expected<CodecEnvelope, CodecError>
encode(CodecVersion declaredRevision, T const& value)
{
auto const wire =
detail::wireVersionFor(declaredRevision, detail::codecTypeCodeV<T>);
if (!wire)
{
return Unexpected{wire.error()};
}
return detail::encodeAt<T, RegisteredCodecs>(*wire, value);
}
// Explicit instantiation for every registered type — the closed set.
#define CODEC_TYPE(CppType, TypeCode) \
template Expected<CppType, CodecError> decode<CppType>( \
CodecEnvelope const&); \
template Expected<CodecEnvelope, CodecError> encode<CppType>( \
CodecVersion, CppType const&);
#include <xrpl/tx/wasm/detail/codec_types.macro>
} // namespace xrpl::wasm

View File

@@ -0,0 +1,74 @@
#include <xrpl/tx/wasm/detail/CodecRevisions.h>
#include <xrpl/tx/wasm/detail/CodecTrait.h>
#include "CodecTypes.h"
#include <array>
#include <utility>
// ───────────────────────────────────────────────────────────────────────────
// Revision / amendment POLICY.
//
// The CodecTrait<N> revision maps and the wireVersionFor switch are BOTH
// generated from codec_revisions.macro, exactly as the (type, version)
// registry drives the dispatch tables — so the maps and the dispatch can't
// drift from the registration list.
// ───────────────────────────────────────────────────────────────────────────
namespace xrpl::wasm::detail {
// (1) Generate the CodecTrait<N> revision maps.
#define CODEC_REVISION_BEGIN(Revision) \
template <> \
struct CodecTrait<Revision> \
{ \
static constexpr CodecVersion kVersion = Revision; \
static constexpr auto versions = \
std::to_array<std::pair<CodecTypeCode, CodecVersion>>({
#define CODEC_REVISION_ENTRY(CppType, Version) \
{codecTypeCodeV<CppType>, Version},
#define CODEC_REVISION_END(Revision) \
}); \
};
#include <xrpl/tx/wasm/detail/codec_revisions.macro>
namespace {
template <CodecVersion N>
Expected<CodecVersion, CodecError>
versionFromRevision(CodecTypeCode code)
{
for (auto const& [c, v] : CodecTrait<N>::versions)
{
if (c == code)
{
return v;
}
}
return Unexpected{CodecError::TypeNotInRevision};
}
} // namespace
Expected<CodecVersion, CodecError>
wireVersionFor(CodecVersion declaredRevision, CodecTypeCode code)
{
// TODO(amendment-gating): each revision > 0 must additionally be gated
// behind its amendment (via Rules) before a node may emit it, so no node
// emits a wire version its peers can't agree on. Thread Rules in here.
// (2) Generate the runtime-revision -> CodecTrait<N> dispatch.
switch (declaredRevision)
{
#define CODEC_REVISION_BEGIN(Revision) \
case Revision: \
return versionFromRevision<Revision>(code);
#define CODEC_REVISION_ENTRY(CppType, Version)
#define CODEC_REVISION_END(Revision)
#include <xrpl/tx/wasm/detail/codec_revisions.macro>
default:
return Unexpected{CodecError::UnknownCodecVersion};
}
}
} // namespace xrpl::wasm::detail

View File

@@ -0,0 +1,34 @@
#pragma once
//
// Internal aggregation header for the wasm codec registry (NOT public — lives
// under src/). Expands codec_types.macro to declare, in one shared place:
//
// * CODEC_TYPE -> the C++ type -> CodecTypeCode mapping
// * CODEC_VERSION -> each (type, version) CodecTypeTrait specialization
//
// Both the registry TU and every per-type codec .cpp include this, so the trait
// declarations they share are always generated from the same .def and can never
// drift.
//
#include <xrpl/basics/Expected.h> // used by the trait macro's decode/encode
#include <xrpl/tx/wasm/detail/CodecTypeCode.h>
#include <xrpl/tx/wasm/detail/CodecTypeTrait.h>
// One include per registered C++ *type* (not per version):
#include <xrpl/basics/Number.h>
#include <xrpl/protocol/AccountID.h>
namespace xrpl::wasm::detail {
// type -> type code
#define CODEC_TYPE(CppType, TypeCode) XRPL_WASM_CODEC_TYPE_CODE(CppType, TypeCode)
#include <xrpl/tx/wasm/detail/codec_types.macro>
// per-(type, version) trait declarations (bodies live in the type's .cpp)
#define CODEC_VERSION(CppType, Version, PayloadSize) \
XRPL_WASM_CODEC_TYPE_TRAIT(CppType, Version, PayloadSize)
#include <xrpl/tx/wasm/detail/codec_types.macro>
} // namespace xrpl::wasm::detail

View File

@@ -0,0 +1,54 @@
#include "CodecTypes.h"
#include <xrpl/protocol/Serializer.h>
#include <cstdint>
#include <limits>
// ───────────────────────────────────────────────────────────────────────────
// Codec bodies for Number. One decode/encode pair per registered version.
// (GLOB_RECURSE compiles this file automatically; it is wired into the dispatch
// tables by the explicit instantiations in CodecRegistry.cpp.)
// ───────────────────────────────────────────────────────────────────────────
namespace xrpl::wasm::detail {
// ── Number, version 0 : int64 mantissa + int32 exponent = 12 bytes ──────────
// Layout deliberately matches the frozen on-ledger STNumber serialization.
template <>
Expected<Number, CodecError>
CodecTypeTrait<codecTypeCodeV<Number>, 0>::decode(CodecEnvelope const& env)
{
if (env.header.payload_size != kPayloadSize)
{
return Unexpected{CodecError::Truncated};
}
auto sit = SerialIter{env.payload, env.header.payload_size};
auto const mantissa = sit.geti64();
auto const exponent = sit.geti32();
return Number{mantissa, exponent};
}
template <>
Expected<CodecEnvelope, CodecError>
CodecTypeTrait<codecTypeCodeV<Number>, 0>::encode(Number const& value)
{
// v0 carries an int64 mantissa; Number's mantissa is int64 today so it
// always fits. The guard documents the boundary for a future wider Number
// (the "graceful fail" path when a value can't be represented in v0).
if (value.exponent() < std::numeric_limits<std::int32_t>::min() ||
value.exponent() > std::numeric_limits<std::int32_t>::max())
{
return Unexpected{CodecError::NotRepresentable};
}
// TODO(payload-ownership): the returned envelope's payload must reference
// storage that outlives the call; that ownership belongs to the wire layer.
// The encode signature is intentionally left as-is for now, so the actual
// byte production is deferred there.
return CodecEnvelope{.header={.type_code=kCodecTypeCode, .version=kVersion, .payload_size=kPayloadSize}, .payload=nullptr};
}
} // namespace xrpl::wasm::detail