Skip to content

De-generify Sign methods #861

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 9, 2021

Conversation

devrandom
Copy link
Member

@devrandom devrandom commented Apr 1, 2021

This allows all methods of Sign (herein renamed to BaseSign) to be non-generic, which allows it to have a vtable and be a dyn object.

In general, the following can prevent a trait for being used with dyn:

  • any of the trait methods is generic
  • the trait requires Sized, directly or indirectly (e.g. via Clone)

@devrandom devrandom requested review from TheBlueMatt and jkczyz April 1, 2021 08:49
@devrandom devrandom force-pushed the degenerify branch 2 times, most recently from f0df509 to 13c733e Compare April 1, 2021 08:58
@codecov
Copy link

codecov bot commented Apr 1, 2021

Codecov Report

Merging #861 (0821fb3) into main (e23c270) will increase coverage by 0.29%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #861      +/-   ##
==========================================
+ Coverage   90.60%   90.89%   +0.29%     
==========================================
  Files          51       51              
  Lines       27295    28351    +1056     
==========================================
+ Hits        24730    25769    +1039     
- Misses       2565     2582      +17     
Impacted Files Coverage Δ
lightning/src/ln/channel.rs 87.32% <ø> (ø)
lightning/src/ln/functional_tests.rs 97.00% <ø> (+0.25%) ⬆️
lightning-persister/src/lib.rs 97.31% <100.00%> (+1.69%) ⬆️
lightning/src/chain/keysinterface.rs 93.62% <100.00%> (+0.02%) ⬆️
lightning/src/util/enforcing_trait_impls.rs 90.09% <100.00%> (ø)
lightning/src/routing/router.rs 96.87% <0.00%> (-0.10%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e23c270...0821fb3. Read the comment docs.

@devrandom
Copy link
Member Author

devrandom commented Apr 1, 2021

Hmm... not sure why the router benchmarks are failing. They are also failing for me in master.

Oh, they are not required.

@devrandom devrandom force-pushed the degenerify branch 2 times, most recently from d36f3a8 to 198f27a Compare April 1, 2021 11:11
@devrandom devrandom changed the title De-generify Writer De-generify Sign methods Apr 1, 2021
Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Hmmmmm, is there a way to avoid the dropping of Sign: Clone? I'm not sure there is a way to avoid requiring ChannelMonitor: Clone in the Java bindings (as there is no other way to give java a reference to something in Rust that is only valid for the scope of a function call, which we require for ChannelMonitor), I just haven't spent the time to figure out if I want to upstream that or not :/.

If its really required, maybe there's some other clever way around it - obviously the "Rust-y" way of doing the dyn would be to enum a wrapper, though it requires a few layers of wrappers so may be more challenging here. Or maybe there's a way to have a dynamic clone-but-return-the-boxed-one thing that I'm not aware of (we could always define our own trait for that).

enum DynamicSigner {
    A(SignerImplA), B(SignerImplB)
}
impl Sign for DynamicSigner {
  fn f(&self) { match self { A(a) => a.f(), B(b) -> b.f() } }
}

@devrandom
Copy link
Member Author

devrandom commented Apr 1, 2021

Hmmmmm, is there a way to avoid the dropping of Sign: Clone? I'm not sure there is a way to avoid requiring ChannelMonitor: Clone in the Java bindings (as there is no other way to give java a reference to something in Rust that is only valid for the scope of a function call, which we require for ChannelMonitor), I just haven't spent the time to figure out if I want to upstream that or not :/.

Might be able to impl Clone for Box<Signer>. Would it be acceptable to Box it in ChannelMonitor? I assume there's no memory allocation concerns because these are somewhat long lived.

Decided this is not needed, see BaseSign instead.

@TheBlueMatt
Copy link
Collaborator

Might be able to impl Clone for Box. Would it be acceptable to Box it in ChannelMonitor? I assume there's no memory allocation concerns because these are somewhat long lived.

Correct, if you are using a Box<Signer> that work work, but I think that would be hard, too, but I haven't ever dug into that too deeply.

@TheBlueMatt
Copy link
Collaborator

Heh, sometimes rust is just really stupid, BaseSign looks good, though, can you add docs for it? I think this PR otherwise looks basically good, mod #861 (comment)

@devrandom devrandom force-pushed the degenerify branch 3 times, most recently from 1f5b1ab to 4290706 Compare April 6, 2021 19:27
@devrandom devrandom force-pushed the degenerify branch 3 times, most recently from 1b6e001 to 216f35c Compare April 7, 2021 08:19
Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Looks good, just tiny stuff, really. In general, best to word-wrap your commit messages to ~80 chars.

Copy link

@ariard ariard left a comment

Choose a reason for hiding this comment

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

Sounds good to me, minor comments

1 +
1 + 1 + high_zero_bytes_dropped_length(self.amt_to_forward) +
1 + 1 + high_zero_bytes_dropped_length(self.outgoing_cltv_value as u64) +
1 + 1 + 8 // short_channel_id
Copy link

Choose a reason for hiding this comment

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

What's the variance there, smallest 17 bytes, biggest 27 bytes ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Theoretically, yes, but in practice outgoing_cltv_value needs four bytes on mainnet.

None => 0,
Some(payment_data) =>
1 + 1 + high_zero_bytes_dropped_length(payment_data.total_msat) +
32 // payment_secret
Copy link

Choose a reason for hiding this comment

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

Code-style, what do you think about ?

diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs
index 5e65c9ec..477474d0 100644
--- a/lightning/src/ln/msgs.rs
+++ b/lightning/src/ln/msgs.rs
@@ -909,16 +909,11 @@ mod fuzzy_internal_msgs {
                                        1 +
                                                1 + 1 + high_zero_bytes_dropped_length(self.amt_to_forward) +
                                                1 + 1 + high_zero_bytes_dropped_length(self.outgoing_cltv_value as u64) +
-                                               match payment_data {
-                                                       None => 0,
-                                                       Some(payment_data) =>
-                                                               1 + 1 + high_zero_bytes_dropped_length(payment_data.total_msat) +
-                                                                       32 // payment_secret
-                                               }
+                                               if payment_data.is_none() { 0 } 
+                                               else { 1 + 1 + high_zero_bytes_dropped_length(payment_data.total_msat) + 32 } // payment_secret
                                }
                        }
                }
-
        }
 
        pub struct DecodedOnionErrorPacket {

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, payment_data would need an unwrap in the else branch, which I usually try to avoid if I need to do something in both some/none cases. Just aesthetics, so happy to change it if LDK is standardizing on the if/else form.

Comment on lines -741 to +751
secp_ctx: Secp256k1<secp256k1::SignOnly>,
secp_ctx: Secp256k1<secp256k1::All>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Was this change deliberate? It doesn't seem to go with the commit message.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. I'll add to the commit message.

It just doesn't seem worth it to deal with both kinds of contexts separately, given that the caller might as well have an All handy to be able to supply both for different calls.

I think it's also not possible to pass in an All when Signing is expected without generics, which we are trying to get away from here.

Comment on lines 57 to 59
pub fn size_hint(&mut self, size: usize) {
self.0.reserve(size)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I was looking into how reserve is implemented. It seems it increases the capacity to the max of either the desired capacity or double the current capacity. This makes me wonder how useful size_hint really is.

I suppose we may save some allocations if the writer has 0 capacity, but I'm wondering if the same thing can be accomplished while defining our public interfaces in terms of std::io::Write instead of Writer. For instance, if a message knows it size, we could do something like:

Writer::with_capacity(message.size_hint());

Rather than having the message call size_hint on Writer. Thoughts?

Copy link
Member Author

@devrandom devrandom Apr 8, 2021

Choose a reason for hiding this comment

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

We could do reserve_exact.

But I am guessing that the caller almost always calls these with a zero sized writer, otherwise it's kind of ineffective. If this is called with a non-zero sized writer (e.g. if this object is a field in a larger struct), then, yes, the size_hint mechanism won't be very effective and we should think about moving to something like with_capacity.

Looking at this further, it looks like size_hint is called when serializing sub-structs quite a bit. I wonder how effective it really is for that purpose, given what you say, and also given that the enclosing object would have to estimate the size without the sub-object that also has a hint (otherwise it would be double counting).

Maybe @TheBlueMatt has more insight about how size_hint is meant to be used.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, in general it probably doesn't make sense to use it for sub-structs. It's mostly intended for where we know the exact size, which we generally do in all our P2P message sending, avoiding copying the data a few times. Luckily, the max Jeff referenced means we can't really do worse than the "normal" behavior with it, though.

Copy link
Collaborator

Choose a reason for hiding this comment

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

As for putting it in messages vs calling it on a Writer, I certainly don't care which we end to with, but it should only be one or the other, and this PR is already nontrivial in size.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, in general it probably doesn't make sense to use it for sub-structs.

It seems to be getting called for sub-structs, given that it's called on non-empty Writer instances during the tests. Should I open an issue to look into that?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@TheBlueMatt Not sure I understand how that would look with regards to a ChannelMonitor serializing its parameterized Sign.

Right, I guess there's a few options - take a reference to the KeysManager and call KeysManager::serialize_signer(sign: &Sign). Or we could go with a straightforward Sign::serialize() -> Vec<u8> only.

Copy link
Member Author

@devrandom devrandom Apr 9, 2021

Choose a reason for hiding this comment

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

@devrandom Could DynSigner also have a field storing something like FnOnce that downcasts the boxed signer and calls write? Then DynSigner could implement Writeable by calling this. The instantiator of DynSigner would need to give this function, but presumably that is possible since they are also giving boxed signer and thus know what type to downcast to.

Yes, that works. There are actually more options, since a generic Write can be converted to a dyn Write inside DynSigner.

But in any case, moving Writeable from BaseSign to Sign is definitely a valid approach. I am not worried about the sunk cost of having rewritten the Writeable trait, happy to back that out. Should decide whether or not to do that based on whether we actually expect some advantage from being able to write to arbitrary destinations without going via a Vec intermediary representation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, that works.

Huh! I guess when the limits the language is applying are arbitrary, the workarounds are arbitrary too :).

That looks good to me, too, what do you think @jkczyz?

Copy link
Contributor

Choose a reason for hiding this comment

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

Huh! I guess when the limits the language is applying are arbitrary, the workarounds are arbitrary too :).

That looks good to me, too, what do you think @jkczyz?

Works for me! Would be nice to move Writeable off to where it is needed rather than making it a sub-trait, if that's possible. Wasn't quite clear if the workaround allowed for that. But either way seems clean enough.

I am not worried about the sunk cost of having rewritten the Writeable trait, happy to back that out.

@devrandom Regarding Writer trait, I do like making that a struct and using std::io::Write, but that is no longer applicable to this PR and can come at some later time, as you said. I think you meant Writer here, at least.

@devrandom
Copy link
Member Author

This is ready for another look.

The generic methods prevent Sign from being a dyn object.

Use Secp256k1<All> as part of removing generics from Secp256k1 contexts passed into Sign methods.
Clone requires Sized, which prevents Sign from being a dyn object.
@devrandom
Copy link
Member Author

OK, the changes to Writer / Writeable have been taken out, and Writeable is now part of Sign instead of BaseSign. This was as a result of the discussion in #861 (comment).

This is ready for another review.

@ariard
Copy link

ariard commented Apr 9, 2021

Code Review ACK 0821fb3

#[test]
pub fn dyn_sign() {
let _signer: Box<dyn BaseSign>;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: add newline

@TheBlueMatt TheBlueMatt merged commit dba0709 into lightningdevkit:main Apr 9, 2021
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.

4 participants