Skip to content

Conversation

@rorp
Copy link
Contributor

@rorp rorp commented Oct 9, 2025

Summary

This PR extracts the core oracle event creation and signing logic into standalone free functions, improving code reusability and testability while maintaining the existing Oracle API.

Major Changes

Function Extraction

  • create_enum_event() - Standalone function for creating enum event announcements
  • sign_enum_event() - Standalone function for signing enum event outcomes
  • create_numeric_event() - Standalone function for creating numeric event announcements
  • sign_numeric_event() - Standalone function for signing numeric event outcomes with clamping logic
  • Helper functions: get_min_value(), get_max_value(), derive_signing_key()

Architectural Benefits

  • Code Reusability: Core DLC logic can be used independently of the Oracle struct
  • Testability: Free functions are easier to unit test in isolation
  • Separation of Concerns: DLC oracle logic separated from storage/persistence concerns
  • API Flexibility: Users can choose between high-level Oracle methods or low-level functions

Implementation Details

  • Oracle methods now delegate to the extracted free functions
  • Maintains backward compatibility - all existing Oracle API remains unchanged
  • Enhanced error handling with proper validation in free functions
  • Comprehensive input validation for edge cases (empty strings, invalid ranges, etc.)

Minor Improvements

  • Documentation: Added comprehensive Rust docs for all public functions
  • Unit Tests: Added focused tests for free functions
  • DLC Spec References: Linked to relevant DLC specification sections

Impact

  • No Breaking Changes: All existing code continues to work unchanged
  • Enhanced Flexibility: Developers can now use either high-level or low-level APIs
  • Better Testing: Isolated unit tests for core cryptographic operations
  • Improved Maintainability: Clear separation between DLC logic and infrastructure

Testing

  • All existing tests pass without modification
  • New unit tests cover free functions and edge cases
  • Clamping behavior thoroughly tested for both signed and unsigned numeric events
  • This refactor significantly improves the codebase architecture while maintaining full backward compatibility, making the oracle system more modular and testable.

@rorp rorp force-pushed the refactor_kormir branch 2 times, most recently from 8031aa2 to 26ca2d8 Compare October 9, 2025 23:32
@kwsantiago
Copy link
Contributor

Take a look at #104 in case there's anything you can grab from there. I can close 104, pending @bennyhodl

Copy link
Contributor

@kwsantiago kwsantiago left a comment

Choose a reason for hiding this comment

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

Good code quality, excellent tests, solid documentation. Left some comments for silent behavior change from errors to clamping.

if event_id.is_empty() {
return Err(Error::InvalidArgument);
}
if base != 2 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Base 2 is hardcoded but the function accepts a base parameter.

Either remove the parameter or support other bases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm planning to add support for other bases in a separate PR.

if base != 2 {
return Err(Error::InvalidArgument);
}
if num_digits == 0 || num_digits > 63 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why 63?

This should be a constant with an explanation of why this number was chosen.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because we use i64 as outcome type which uses 63 bits for the value and 1 bit for the sign. Probably it makes sense to use BigInt and BigUint here. But again, this should be done in a separate PR.

);

// verify our nonce is the same as the one in the announcement
debug_assert!(sig.encode()[..32] == announcement.oracle_event.oracle_nonces[0].serialize());
Copy link
Contributor

Choose a reason for hiding this comment

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

The old code used an iterator while the new code is collecting all nonce keys which could lead to a small performance regression for large nonce sets.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we need an iterator to collect 1 nonce.

Copy link
Owner

Choose a reason for hiding this comment

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

We should at least check that we have a nonce. Validating the announcement will check that we have 1 nonce.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In fact we don't need this line at all. It was added to check if rust-dlc signing algorithm worked as expected. And it did.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved these checks to the unit tests.

oracle_nonces,
event_id,
let nonce_indexes = self.storage.get_next_nonce_indexes(1).await?;
if nonce_indexes.len() != 1 {
Copy link
Contributor

Choose a reason for hiding this comment

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

This check seems defensive but the free function doesn't do it. There is inconsistent validation between Oracle methods and free functions.

Copy link
Contributor Author

@rorp rorp Nov 24, 2025

Choose a reason for hiding this comment

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

Of course the free function doesn't do it. The compiler checks it. This was not an inconsistency, but a bug in kormir, which could lead to unexpected panics.

/// * [Oracle Announcement](https://github.com/discreetlogcontracts/dlcspecs/blob/master/Messaging.md#the-oracle_announcement-type)
/// * [Oracle Event](https://github.com/discreetlogcontracts/dlcspecs/blob/master/Messaging.md#the-oracle_event-type)
/// * [Digit Decomposition Event Descriptor](https://github.com/discreetlogcontracts/dlcspecs/blob/master/Messaging.md#digit_decomposition_event_descriptor)
#[allow(clippy::too_many_arguments)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: consider a builder pattern or config struct instead of suppressing the lint.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea for a separate PR.

@rorp
Copy link
Contributor Author

rorp commented Nov 24, 2025

Take a look at #104 in case there's anything you can grab from there. I can close 104, pending @bennyhodl

This PR and #104 are orthogonal.

#104 creates CLI commands that allow users to connect to a Kormir server instance in order for the server to create announcements and attestations.

This PR extracts the core logic of creating announcements and attestations so it can be used in other orcale apps. Imagine a weather web-site, that posts announcements on Twitter; attests them with the data from their internal database and again posts attestations on Twitter; and uses their custom private key setup without exposing sign- endpoints. This PR makes it easier for them to build this kind of app.

@rorp rorp force-pushed the refactor_kormir branch 2 times, most recently from 0cf7156 to a7c14a9 Compare November 25, 2025 02:18
@rorp rorp requested a review from kwsantiago November 25, 2025 02:25
Copy link
Owner

@bennyhodl bennyhodl left a comment

Choose a reason for hiding this comment

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

thanks 🤙🏼

small nits

);

// verify our nonce is the same as the one in the announcement
debug_assert!(sig.encode()[..32] == announcement.oracle_event.oracle_nonces[0].serialize());
Copy link
Owner

Choose a reason for hiding this comment

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

We should at least check that we have a nonce. Validating the announcement will check that we have 1 nonce.

return Err(Error::InvalidArgument);
}
if key_pair.x_only_public_key().0 != announcement.oracle_public_key {
return Err(Error::InvalidAnnouncement);
Copy link
Owner

Choose a reason for hiding this comment

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

InvalidAnnouncement might be the wrong error here. The signature can still be valid for that public key

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't want to attest someone else's announcements. This check validates if it's the case.

Comment on lines +202 to +215
// verify our signature
if secp
.verify_schnorr(&sig, &msg, &key_pair.x_only_public_key().0)
.is_err()
{
return Err(Error::Internal);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

The old code verified that the nonce in the signature matched the announcement's nonce.

Should we add a runtime check to ensure the caller passed the correct nonce key matching the announcement?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! I added an explicit check for the nonces and nonce keys.

&data.announcement,
outcome,
&nonce_keys,
false,
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't clamp_outcome be exposed as an optional param instead of hard-coding to false? If it's always set to false then the Oracle struct can never use clamping.

Copy link
Contributor Author

@rorp rorp Nov 30, 2025

Choose a reason for hiding this comment

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

It shouldn't be there in the first place. The oracle must be spec compliant. Reverted it to the initial commit.

/// Kormir error type
#[derive(Debug, Clone)]
pub enum Error {
/// Invalid argument given
Copy link
Contributor

Choose a reason for hiding this comment

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

Since InvalidArgument was removed, this is a breaking change for anyone using Error::InvalidArgument

May want to add in changelog. The new errors you added are better though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverted it and made it deprecated. Will be removed in the next minor release.

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