Skip to content

Add new float host functions and versioning rules#504

Open
sappenin wants to merge 9 commits intomasterfrom
df/update-host-functions
Open

Add new float host functions and versioning rules#504
sappenin wants to merge 9 commits intomasterfrom
df/update-host-functions

Conversation

@sappenin
Copy link
Copy Markdown
Collaborator

  • Updated §5.8 (Floats) preamble to describe OpaqueFloat as a general-purpose 12-byte buffer (4-byte exponent + 8-byte mantissa), document the rounding_modes values, and drop the IOU-specific framing
  • Added six new host functions to §5.8 (all at 1000 gas, consistent with existing float operations)
  • Added §5.11 Host Function Versioning Rules, covering the immutability of deployed host functions and the requirement to introduce new function names if buffer layouts change

- Updated §5.8 (Floats) preamble to describe OpaqueFloat as a general-purpose 12-byte buffer (4-byte exponent + 8-byte mantissa), document the rounding_modes values, and drop the IOU-specific framing
- Added six new host functions to §5.8 (all at 1000 gas, consistent with existing float operations)
- Added §5.11 Host Function Versioning Rules, covering the immutability of deployed host functions and the requirement to introduce new function names if buffer layouts change

Signed-off-by: David Fuelling <sappenin@gmail.com>
@sappenin sappenin requested a review from mvadari March 23, 2026 23:54
Signed-off-by: David Fuelling <sappenin@gmail.com>
new function are unaffected.
2. **Host functions MAY be deprecated** with appropriate notice, but deprecated functions MUST remain callable for
backward compatibility. Deployed contracts may rely on any host function that was available at deployment time.
3. **Host functions MUST NOT ever be changed.** Once a host function is deployed — its name, parameter types,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what I think about this rule. E.g., an amendment might change the meaning of a host function, but then again... should that be allowed? Such a thing would likely break existing contracts assuming the previous functionality.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is hard to think... I think these are the right requirements. I.e. it is essentially append-only. Unfortunately I don't think there is a gate to guard this. The amendment (at least the existing meaning I understand) is not for this. I think the current approach about the float is the right one, "only use exponent and mantissa if want to store the float".

Copy link
Copy Markdown
Collaborator Author

@sappenin sappenin Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brainstorming out loud, here are some possible approaches that might help us version host functions to better ensure correctness.

1. Explicit versioning per host function
Contract authors declare which version they target (e.g. float_set/v1). If rippled introduces float_set/v2, the contract keeps calling v1 until explicitly upgraded. Downside: rippled can never remove old host functions, and it adds overhead for contract authors.

2. Host-function fingerprint
Hash all host-function names+versions into a single fingerprint, stored in each contract (similar to Data). rippled computes and caches its own fingerprint at runtime; if they diverge, the contract enters a permanent-fail mode. This is blunt and could create pain for contract authors who must react to any host-function change — but it enforces correctness. Scoping the fingerprint strictly to host functions (not all amendments) keeps it manageable.

3. "Do nothing for now" is also an option, but feels risky: by the time we need a versioning system, older contracts will have no way to adopt it retroactively.

4. Stable core + versioned extensions: Define a small, frozen set of core host functions (arithmetic, ledger reads, etc.) that are guaranteed never to change. Everything experimental or evolving lives in a separate, explicitly versioned extension namespace. Contracts that only use the core never think about versioning; only contracts using extensions need to care.

5. Runtime capability query: Expose a host function like get_capabilities() -> u64 that returns a bitmask of supported features. Contracts can branch at runtime based on what's available — similar to CPU feature detection (SSE, AVX). This is flexible but pushes complexity into every contract that needs to handle multiple code paths.

@pwang200
Copy link
Copy Markdown

Because a smart escrow should store the mantissa and exponent as separate integers in a contract-defined format if the smart escrow needs to persist the opaque float in the ledger. It needs a way to get the mantissa and exponent, through a new host function:

float_to_mantissa_and_exponent(
 opaque_float_in_buf: i32,
 opaque_float_in_len: i32,
 mantissa_out_buf: i32,
 mantissa_out_len: i32,
 exponent_out_buf: i32,
 exponent_out_len: i32,
)

Does it need a rounding mode? Not, right?

Signed-off-by: David Fuelling <sappenin@gmail.com>
Signed-off-by: David Fuelling <sappenin@gmail.com>
`float_from_stamount` and `float_from_stnumber` exist to load values from those on-ledger formats into `OpaqueFloat`
for in-contract computation, without touching how those values are stored or transmitted on the wire.

### C.9: Why is host function immutability required?
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is probably what we want, but open to other solutions.


The 4 extra bytes per value are negligible given the `no_std` stack-only model.

**Compared to STNumber (14 bytes):** OpaqueFloat is 2 bytes shorter because it omits the type prefix — the host
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current "12 byte" serialization does save two bytes as compared to just using STNumber (which is likely 14 bytes). However, if we did just use STNumber bytes, then we might be able to remove the need for float_from_stnumber since an stnumber would be a float.

Thoughts?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion. My vote is the "12 byte".

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also lean towards "keep 12" but am noticing that -- in Rust/WASM -- anytime I grab an STNumber, I can't just use it. I need to convert it to an OpaqueFloat. It would instead be nice if the WASM layer just treated those as the same thing, so I can save a host function call.

Signed-off-by: David Fuelling <sappenin@gmail.com>
@sappenin
Copy link
Copy Markdown
Collaborator Author

@pwang200 I addressed your comment above via e56c1b9

| `float_multiply(`<br/>&emsp;`in_buf1: i32,`<br/>&emsp;`in_len1: i32,`<br/>&emsp;`in_buf2: i32,`<br/>&emsp;`in_len2: i32,`<br/>&emsp;`out_buf: i32,`<br/>&emsp;`out_len: i32,`<br/>&emsp;`rounding_modes: i32`<br />`)` | Multiply two floats in rippled format. | 1000 |
| `float_divide(`<br/>&emsp;`in_buf1: i32,`<br/>&emsp;`in_len1: i32,`<br/>&emsp;`in_buf2: i32,`<br/>&emsp;`in_len2: i32,`<br/>&emsp;`out_buf: i32,`<br/>&emsp;`out_len: i32,`<br/>&emsp;`rounding_modes: i32`<br />`)` | Divide two floats in rippled format. | 1000 |
| `float_negate(`<br/>&emsp;`in_buf: i32,`<br/>&emsp;`in_len: i32,`<br/>&emsp;`out_buf: i32,`<br/>&emsp;`out_len: i32`<br />`)` | Negate a float. | 1000 |
| `float_abs(`<br/>&emsp;`in_buf: i32,`<br/>&emsp;`in_len: i32,`<br/>&emsp;`out_buf: i32,`<br/>&emsp;`out_len: i32`<br />`)` | Absolute value of a float. | 1000 |
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need float_abs? We can accomplish the same thing checking with float_sign and then float_negate.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion. My vote is keep it if it is there already. Same for other host functions.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I feel the same (weakly-held opinion). @mvadari chime in here, and if you agree we can resolve this comment and the onle above about float_negate.

| `float_subtract(`<br/>&emsp;`in_buf1: i32,`<br/>&emsp;`in_len1: i32,`<br/>&emsp;`in_buf2: i32,`<br/>&emsp;`in_len2: i32,`<br/>&emsp;`out_buf: i32,`<br/>&emsp;`out_len: i32,`<br/>&emsp;`rounding_modes: i32`<br />`)` | Subtract two floats in rippled format. | 1000 |
| `float_multiply(`<br/>&emsp;`in_buf1: i32,`<br/>&emsp;`in_len1: i32,`<br/>&emsp;`in_buf2: i32,`<br/>&emsp;`in_len2: i32,`<br/>&emsp;`out_buf: i32,`<br/>&emsp;`out_len: i32,`<br/>&emsp;`rounding_modes: i32`<br />`)` | Multiply two floats in rippled format. | 1000 |
| `float_divide(`<br/>&emsp;`in_buf1: i32,`<br/>&emsp;`in_len1: i32,`<br/>&emsp;`in_buf2: i32,`<br/>&emsp;`in_len2: i32,`<br/>&emsp;`out_buf: i32,`<br/>&emsp;`out_len: i32,`<br/>&emsp;`rounding_modes: i32`<br />`)` | Divide two floats in rippled format. | 1000 |
| `float_negate(`<br/>&emsp;`in_buf: i32,`<br/>&emsp;`in_len: i32,`<br/>&emsp;`out_buf: i32,`<br/>&emsp;`out_len: i32`<br />`)` | Negate a float. | 1000 |
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need float_negate? We can accomplish the same things by declaring -1 and then using float_multiply.

| `float_sign(`<br/>&emsp;`in_buf: i32,`<br/>&emsp;`in_len: i32`<br />`)` | Sign of a float. Returns `-1` (negative), `0` (zero), or `1` (positive). | 1000 |
| `float_pow(`<br/>&emsp;`in_buf: i32,`<br/>&emsp;`in_len: i32,`<br/>&emsp;`pow: i32,`<br/>&emsp;`out_buf: i32,`<br/>&emsp;`out_len: i32,`<br/>&emsp;`rounding_modes: i32`<br />`)` | Compute the nth power of a float in rippled format. | 1000 |
| `float_root(`<br/>&emsp;`in_buf: i32,`<br/>&emsp;`in_len: i32,`<br/>&emsp;`root: i32,`<br/>&emsp;`out_buf: i32,`<br/>&emsp;`out_len: i32,`<br/>&emsp;`rounding_modes: i32`<br />`)` | Compute the nth root of a float in rippled format. | 1000 |
| `float_log(`<br/>&emsp;`in_buf: i32,`<br/>&emsp;`in_len: i32,`<br/>&emsp;`out_buf: i32,`<br/>&emsp;`out_len: i32,`<br/>&emsp;`rounding_modes: i32`<br />`)` | Compute the 10 based log of a float in rippled format. | 1000 |
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is float_log necessary?

| Function Signature | Description | Gas Cost |
|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------|:---------|
| `float_from_int(`<br/>&emsp;`in_int: i64,`<br/>&emsp;`out_buf: i32,`<br/>&emsp;`out_len: i32,`<br/>&emsp;`rounding_modes: i32`<br />`)` | Create a float in rippled format from a 64-bit integer. | 1000 |
| `float_from_uint(`<br/>&emsp;`in_uint_ptr: i32,`<br/>&emsp;`in_uint_len: i32,`<br/>&emsp;`out_buf: i32,`<br/>&emsp;`out_len: i32,`<br/>&emsp;`rounding_modes: i32`<br />`)` | Create a float in rippled format from a 64-bit unsigned integer. | 1000 |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the gas costs of the float host functions mismatches with the code:

    WASM_IMPORT_FUNC2(i, floatFromInt, "float_from_int", hfs,                                                  100);
    WASM_IMPORT_FUNC2(i, floatFromUint, "float_from_uint", hfs,                                                130);
    WASM_IMPORT_FUNC2(i, floatFromSTAmount, "float_from_stamount", hfs,                                        150);
    WASM_IMPORT_FUNC2(i, floatFromSTNumber, "float_from_stnumber", hfs,                                        150);
    WASM_IMPORT_FUNC2(i, floatToInt, "float_to_int", hfs,                                                      130);
    WASM_IMPORT_FUNC2(i, floatToMantissaAndExponent, "float_to_mantissa_and_exponent", hfs,                    130);
    WASM_IMPORT_FUNC2(i, floatNegate, "float_negate", hfs,                                                     150);
    WASM_IMPORT_FUNC2(i, floatAbs, "float_abs", hfs,                                                           150);
    WASM_IMPORT_FUNC2(i, floatSet, "float_set", hfs,                                                           100);
    WASM_IMPORT_FUNC2(i, floatCompare, "float_compare", hfs,                                                    80);
    WASM_IMPORT_FUNC2(i, floatAdd, "float_add", hfs,                                                           160);
    WASM_IMPORT_FUNC2(i, floatSubtract, "float_subtract", hfs,                                                 160);
    WASM_IMPORT_FUNC2(i, floatMultiply, "float_multiply", hfs,                                                 300);
    WASM_IMPORT_FUNC2(i, floatDivide, "float_divide", hfs,                                                     300);
    WASM_IMPORT_FUNC2(i, floatRoot, "float_root", hfs,                                                       5'500);
    WASM_IMPORT_FUNC2(i, floatPower, "float_pow", hfs,                                                       5'500);
    WASM_IMPORT_FUNC2(i, floatLog, "float_log", hfs,                                                        12'000);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, fixed via 669e9c5

- **Exponent** (bytes 0–3): Signed 32-bit integer (`i32`), big-endian. Represents the power of 10 applied to the
mantissa.
- **Mantissa** (bytes 4–11): Signed 64-bit integer (`i64`), big-endian. Represents the significant digits of the value.
When normalized: 10^15 ≤ |mantissa| < 10^16 (i.e., 1,000,000,000,000,000 to 9,999,999,999,999,999), except for zero.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code used 10^18 ≤ |mantissa| < 10^19

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed via 19b88f2

Signed-off-by: David Fuelling <sappenin@gmail.com>
Signed-off-by: David Fuelling <sappenin@gmail.com>
Signed-off-by: David Fuelling <sappenin@gmail.com>

| Function Signature | Description | Gas Cost |
| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------- | :------- |
| `float_from_int(`<br/>&emsp;`in_int: i64,`<br/>&emsp;`out_buf: i32,`<br/>&emsp;`out_len: i32,`<br/>&emsp;`rounding_modes: i32`<br />`)` | Create a float in rippled format from a 64-bit integer. | 100 |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hope you don't mind a comment from the community. I was wondering why out_buf and out_len are required? I thought lengths would remain the same?

From a contract writer perspective, something like fn xfloat_from_int(i64) with default rounding more nearest and other function for fn xfloat_from_int_up(i64)... or at least use an enum for the rounding mode? And xfloat stands for xrpl float.

My 0.02 RLUSD cents

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants