add peer-pubkeys endpoint

add LocalChannel class
This commit is contained in:
Carsten Otto
2021-11-12 13:24:04 +01:00
parent 367119c8aa
commit 2ec2ecce9a
13 changed files with 185 additions and 61 deletions

View File

@@ -9,12 +9,17 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import java.util.Set;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_3;
import static de.cotto.lndmanagej.model.LocalChannelFixtures.LOCAL_CHANNEL;
import static de.cotto.lndmanagej.model.LocalChannelFixtures.LOCAL_CHANNEL_3;
import static de.cotto.lndmanagej.model.LocalChannelFixtures.LOCAL_CHANNEL_TO_NODE_3;
import static de.cotto.lndmanagej.model.NodeFixtures.ALIAS;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_3;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@@ -47,7 +52,7 @@ class LegacyControllerIT {
@Test
void getOpenChannelIds() throws Exception {
when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(List.of(CHANNEL_ID, CHANNEL_ID_3));
when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_CHANNEL, LOCAL_CHANNEL_3));
mockMvc.perform(get("/legacy/node/" + PUBKEY + "/open-channels"))
.andExpect(content().string(CHANNEL_ID + "\n" + CHANNEL_ID_3));
}
@@ -63,4 +68,11 @@ class LegacyControllerIT {
when(ownNodeService.isSyncedToChain()).thenReturn(false);
mockMvc.perform(get("/legacy/synced-to-chain")).andExpect(content().string("false"));
}
@Test
void getPeerPubkeys() throws Exception {
when(channelService.getOpenChannels()).thenReturn(Set.of(LOCAL_CHANNEL, LOCAL_CHANNEL_TO_NODE_3));
mockMvc.perform(get("/legacy/peer-pubkeys"))
.andExpect(content().string(PUBKEY_2 + "\n" + PUBKEY_3));
}
}

View File

@@ -1,6 +1,8 @@
package de.cotto.lndmanagej.controller;
import de.cotto.lndmanagej.model.Channel;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.LocalChannel;
import de.cotto.lndmanagej.model.Pubkey;
import de.cotto.lndmanagej.service.ChannelService;
import de.cotto.lndmanagej.service.NodeService;
@@ -34,10 +36,21 @@ public class LegacyController {
@GetMapping("/node/{pubkey}/open-channels")
public String getOpenChannelIds(@PathVariable Pubkey pubkey) {
return channelService.getOpenChannelsWith(pubkey).stream()
.map(Channel::getId)
.sorted()
.map(ChannelId::toString)
.collect(Collectors.joining(NEWLINE));
}
@GetMapping("/peer-pubkeys")
public String getPeerPubkeys() {
return channelService.getOpenChannels().stream()
.map(LocalChannel::getRemotePubkey)
.map(Pubkey::toString)
.sorted()
.collect(Collectors.joining(NEWLINE));
}
@GetMapping("/synced-to-chain")
public boolean syncedToChain() {
return ownNodeService.isSyncedToChain();

View File

@@ -4,53 +4,49 @@ import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import de.cotto.lndmanagej.grpc.GrpcChannels;
import de.cotto.lndmanagej.model.Channel;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.Node;
import de.cotto.lndmanagej.model.LocalChannel;
import de.cotto.lndmanagej.model.Pubkey;
import org.springframework.stereotype.Component;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Component
public class ChannelService {
private static final int MAXIMUM_SIZE = 500;
private static final int CACHE_EXPIRY_MINUTES = 5;
private static final int CACHE_EXPIRY_MINUTES = 1;
private final GrpcChannels grpcChannels;
private final LoadingCache<Pubkey, List<ChannelId>> channelsWithPeerCache;
private final LoadingCache<String, Set<LocalChannel>> channelsCache;
public ChannelService(GrpcChannels grpcChannels) {
this.grpcChannels = grpcChannels;
CacheLoader<Pubkey, List<ChannelId>> loader = new CacheLoader<>() {
channelsCache = initializeChannelsCache();
}
public Set<LocalChannel> getOpenChannels() {
return channelsCache.getUnchecked("");
}
public Set<LocalChannel> getOpenChannelsWith(Pubkey peer) {
return getOpenChannels().stream()
.filter(c -> peer.equals(c.getRemotePubkey()))
.collect(Collectors.toSet());
}
private LoadingCache<String, Set<LocalChannel>> initializeChannelsCache() {
CacheLoader<String, Set<LocalChannel>> loader = new CacheLoader<>() {
@Nonnull
@Override
public List<ChannelId> load(@Nonnull Pubkey peer) {
return getOpenChannelsWithWithoutCache(peer);
public Set<LocalChannel> load(@Nonnull String ignored) {
return grpcChannels.getChannels();
}
};
channelsWithPeerCache = CacheBuilder.newBuilder()
return CacheBuilder.newBuilder()
.expireAfterWrite(CACHE_EXPIRY_MINUTES, TimeUnit.MINUTES)
.maximumSize(MAXIMUM_SIZE)
.build(loader);
}
public List<ChannelId> getOpenChannelsWith(Pubkey peer) {
return channelsWithPeerCache.getUnchecked(peer);
}
public List<ChannelId> getOpenChannelsWith(Node peer) {
return getOpenChannelsWith(peer.pubkey());
}
public List<ChannelId> getOpenChannelsWithWithoutCache(Pubkey peer) {
return grpcChannels.getChannels().stream()
.filter(c -> c.getPubkeys().contains(peer))
.map(Channel::getId)
.sorted()
.collect(Collectors.toList());
}
}

View File

@@ -1,5 +1,7 @@
package de.cotto.lndmanagej.controller;
import de.cotto.lndmanagej.model.ChannelFixtures;
import de.cotto.lndmanagej.model.LocalChannel;
import de.cotto.lndmanagej.service.ChannelService;
import de.cotto.lndmanagej.service.NodeService;
import de.cotto.lndmanagej.service.OwnNodeService;
@@ -9,12 +11,17 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Set;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_2;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_3;
import static de.cotto.lndmanagej.model.LocalChannelFixtures.LOCAL_CHANNEL;
import static de.cotto.lndmanagej.model.LocalChannelFixtures.LOCAL_CHANNEL_3;
import static de.cotto.lndmanagej.model.NodeFixtures.ALIAS;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_3;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@@ -40,7 +47,15 @@ class LegacyControllerTest {
@Test
void getOpenChannelIds() {
when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(List.of(CHANNEL_ID, CHANNEL_ID_3));
when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_CHANNEL, LOCAL_CHANNEL_3));
assertThat(legacyController.getOpenChannelIds(PUBKEY)).isEqualTo(
CHANNEL_ID + "\n" + CHANNEL_ID_3
);
}
@Test
void getOpenChannelIds_ordered() {
when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_CHANNEL_3, LOCAL_CHANNEL));
assertThat(legacyController.getOpenChannelIds(PUBKEY)).isEqualTo(
CHANNEL_ID + "\n" + CHANNEL_ID_3
);
@@ -57,4 +72,11 @@ class LegacyControllerTest {
when(ownNodeService.isSyncedToChain()).thenReturn(false);
assertThat(legacyController.syncedToChain()).isFalse();
}
@Test
void getPeerPubkeys() {
LocalChannel channel2 = new LocalChannel(ChannelFixtures.create(PUBKEY, PUBKEY_3, CHANNEL_ID_2), PUBKEY);
when(channelService.getOpenChannels()).thenReturn(Set.of(LOCAL_CHANNEL, channel2));
assertThat(legacyController.getPeerPubkeys()).isEqualTo(PUBKEY_2 + "\n" + PUBKEY_3);
}
}

View File

@@ -1,8 +1,8 @@
package de.cotto.lndmanagej.service;
import de.cotto.lndmanagej.grpc.GrpcChannels;
import de.cotto.lndmanagej.model.Channel;
import de.cotto.lndmanagej.model.ChannelFixtures;
import de.cotto.lndmanagej.model.LocalChannel;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -11,12 +11,9 @@ import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Set;
import static de.cotto.lndmanagej.model.ChannelFixtures.CHANNEL;
import static de.cotto.lndmanagej.model.ChannelFixtures.CHANNEL_3;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_2;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_3;
import static de.cotto.lndmanagej.model.NodeFixtures.NODE_2;
import static de.cotto.lndmanagej.model.LocalChannelFixtures.LOCAL_CHANNEL;
import static de.cotto.lndmanagej.model.LocalChannelFixtures.LOCAL_CHANNEL_3;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_3;
@@ -33,26 +30,16 @@ class ChannelServiceTest {
@Test
void getOpenChannelsWith_by_pubkey() {
when(grpcChannels.getChannels()).thenReturn(Set.of(CHANNEL, CHANNEL_3));
assertThat(channelService.getOpenChannelsWith(PUBKEY_2)).containsExactly(CHANNEL_ID, CHANNEL_ID_3);
}
@Test
void getOpenChannelsWith_by_node() {
when(grpcChannels.getChannels()).thenReturn(Set.of(CHANNEL, CHANNEL_3));
assertThat(channelService.getOpenChannelsWith(NODE_2)).containsExactly(CHANNEL_ID, CHANNEL_ID_3);
when(grpcChannels.getChannels()).thenReturn(Set.of(LOCAL_CHANNEL, LOCAL_CHANNEL_3));
assertThat(channelService.getOpenChannelsWith(PUBKEY_2))
.containsExactlyInAnyOrder(LOCAL_CHANNEL, LOCAL_CHANNEL_3);
}
@Test
void getOpenChannelsWith_ignores_channel_to_other_node() {
Channel channel2 = ChannelFixtures.create(PUBKEY, PUBKEY_3, CHANNEL_ID_2);
when(grpcChannels.getChannels()).thenReturn(Set.of(CHANNEL, channel2, CHANNEL_3));
assertThat(channelService.getOpenChannelsWith(NODE_2)).containsExactly(CHANNEL_ID, CHANNEL_ID_3);
}
@Test
void getOpenChannelsWith_ordered() {
when(grpcChannels.getChannels()).thenReturn(Set.of(CHANNEL_3, CHANNEL));
assertThat(channelService.getOpenChannelsWith(NODE_2)).containsExactly(CHANNEL_ID, CHANNEL_ID_3);
LocalChannel localChannel2 = new LocalChannel(ChannelFixtures.create(PUBKEY, PUBKEY_3, CHANNEL_ID_2), PUBKEY);
when(grpcChannels.getChannels()).thenReturn(Set.of(LOCAL_CHANNEL, localChannel2, LOCAL_CHANNEL_3));
assertThat(channelService.getOpenChannelsWith(PUBKEY_2))
.containsExactlyInAnyOrder(LOCAL_CHANNEL, LOCAL_CHANNEL_3);
}
}

View File

@@ -3,6 +3,7 @@ package de.cotto.lndmanagej.grpc;
import de.cotto.lndmanagej.model.Channel;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.Coins;
import de.cotto.lndmanagej.model.LocalChannel;
import de.cotto.lndmanagej.model.Pubkey;
import org.springframework.stereotype.Component;
@@ -23,20 +24,21 @@ public class GrpcChannels {
this.grpcGetInfo = grpcGetInfo;
}
public Set<Channel> getChannels() {
public Set<LocalChannel> getChannels() {
Pubkey ownPubkey = grpcGetInfo.getPubkey().orElseThrow();
return grpcService.getChannels().stream()
.map(lndChannel -> toChannel(lndChannel, ownPubkey))
.collect(toSet());
}
private Channel toChannel(lnrpc.Channel lndChannel, Pubkey ownPubkey) {
return Channel.builder()
private LocalChannel toChannel(lnrpc.Channel lndChannel, Pubkey ownPubkey) {
Channel channel = Channel.builder()
.withChannelId(ChannelId.fromShortChannelId(lndChannel.getChanId()))
.withCapacity(Coins.ofSatoshis(lndChannel.getCapacity()))
.withNode1(ownPubkey)
.withNode2(Pubkey.create(lndChannel.getRemotePubkey()))
.build();
return new LocalChannel(channel, ownPubkey);
}
}

View File

@@ -12,8 +12,8 @@ import java.util.List;
import java.util.Optional;
import static de.cotto.lndmanagej.model.ChannelFixtures.CAPACITY;
import static de.cotto.lndmanagej.model.ChannelFixtures.CHANNEL;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static de.cotto.lndmanagej.model.LocalChannelFixtures.LOCAL_CHANNEL;
import static de.cotto.lndmanagej.model.NodeFixtures.NODE_2;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static org.assertj.core.api.Assertions.assertThat;
@@ -43,7 +43,7 @@ class GrpcChannelsTest {
@Test
void one_channel() {
when(grpcService.getChannels()).thenReturn(List.of(channel()));
assertThat(grpcChannels.getChannels()).containsExactly(CHANNEL);
assertThat(grpcChannels.getChannels()).containsExactly(LOCAL_CHANNEL);
}
private Channel channel() {

View File

@@ -2,7 +2,9 @@ package de.cotto.lndmanagej.model;
import org.springframework.lang.Nullable;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -11,13 +13,16 @@ import static java.util.Objects.requireNonNull;
public class Channel {
private final ChannelId channelId;
private final Coins capacity;
private final Set<Pubkey> pubkeys = new LinkedHashSet<>();
private final Set<Pubkey> pubkeys;
private Channel(ChannelId channelId, Coins capacity, Pubkey pubkey1, Pubkey pubkey2) {
this(channelId, capacity, List.of(pubkey1, pubkey2));
}
protected Channel(ChannelId channelId, Coins capacity, Collection<Pubkey> pubkeys) {
this.channelId = channelId;
this.capacity = Coins.ofMilliSatoshis(capacity.milliSatoshis());
pubkeys.add(pubkey1);
pubkeys.add(pubkey2);
this.pubkeys = new LinkedHashSet<>(pubkeys);
}
public static Builder builder() {
@@ -70,6 +75,9 @@ public class Channel {
}
public Channel build() {
if (requireNonNull(pubkey1).equals(requireNonNull(pubkey2))) {
throw new IllegalArgumentException("Pubkeys must not be the same");
}
return new Channel(
requireNonNull(channelId),
requireNonNull(capacity),

View File

@@ -0,0 +1,23 @@
package de.cotto.lndmanagej.model;
import java.util.Set;
public class LocalChannel extends Channel {
private final Pubkey remotePubkey;
public LocalChannel(Channel channel, Pubkey ownPubkey) {
super(channel.getId(), channel.getCapacity(), channel.getPubkeys());
Set<Pubkey> pubkeys = channel.getPubkeys();
remotePubkey = pubkeys.stream()
.filter(pubkey -> !ownPubkey.equals(pubkey))
.findFirst()
.orElseThrow();
if (!pubkeys.contains(ownPubkey)) {
throw new IllegalArgumentException("Channel must have given pubkey as peer");
}
}
public Pubkey getRemotePubkey() {
return remotePubkey;
}
}

View File

@@ -9,6 +9,7 @@ import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
class ChannelTest {
@@ -63,6 +64,18 @@ class ChannelTest {
);
}
@Test
void builder_identical_pubkeys() {
assertThatIllegalArgumentException().isThrownBy(
() -> Channel.builder()
.withChannelId(CHANNEL_ID)
.withCapacity(CAPACITY)
.withNode1(PUBKEY)
.withNode2(PUBKEY)
.build()
).withMessage("Pubkeys must not be the same");
}
@Test
void builder_with_all_arguments() {
Channel channel = Channel.builder()

View File

@@ -0,0 +1,32 @@
package de.cotto.lndmanagej.model;
import org.junit.jupiter.api.Test;
import static de.cotto.lndmanagej.model.ChannelFixtures.CHANNEL_2;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_3;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
class LocalChannelTest {
@Test
void getRemotePubkey() {
LocalChannel localChannel = new LocalChannel(ChannelFixtures.create(PUBKEY_2, PUBKEY, CHANNEL_ID), PUBKEY);
assertThat(localChannel.getRemotePubkey()).isEqualTo(PUBKEY_2);
}
@Test
void getRemotePubkey_swapped() {
LocalChannel localChannel = new LocalChannel(ChannelFixtures.create(PUBKEY_3, PUBKEY_2, CHANNEL_ID), PUBKEY_3);
assertThat(localChannel.getRemotePubkey()).isEqualTo(PUBKEY_2);
}
@Test
void ownPubkey_not_in_pubkey_set() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new LocalChannel(CHANNEL_2, PUBKEY_3))
.withMessage("Channel must have given pubkey as peer");
}
}

View File

@@ -5,6 +5,7 @@ import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_2;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_3;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_3;
public final class ChannelFixtures {
public static final Coins CAPACITY = Coins.ofSatoshis(21_000_000L);
@@ -12,6 +13,7 @@ public final class ChannelFixtures {
public static final Channel CHANNEL = create(PUBKEY, PUBKEY_2, CHANNEL_ID);
public static final Channel CHANNEL_2 = create(PUBKEY, PUBKEY_2, CHANNEL_ID_2);
public static final Channel CHANNEL_3 = create(PUBKEY, PUBKEY_2, CHANNEL_ID_3);
public static final Channel CHANNEL_TO_NODE_3 = create(PUBKEY, PUBKEY_3, CHANNEL_ID);
private ChannelFixtures() {
// do not instantiate

View File

@@ -0,0 +1,14 @@
package de.cotto.lndmanagej.model;
import static de.cotto.lndmanagej.model.ChannelFixtures.CHANNEL;
import static de.cotto.lndmanagej.model.ChannelFixtures.CHANNEL_2;
import static de.cotto.lndmanagej.model.ChannelFixtures.CHANNEL_3;
import static de.cotto.lndmanagej.model.ChannelFixtures.CHANNEL_TO_NODE_3;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
public class LocalChannelFixtures {
public static final LocalChannel LOCAL_CHANNEL = new LocalChannel(CHANNEL, PUBKEY);
public static final LocalChannel LOCAL_CHANNEL_2 = new LocalChannel(CHANNEL_2, PUBKEY);
public static final LocalChannel LOCAL_CHANNEL_3 = new LocalChannel(CHANNEL_3, PUBKEY);
public static final LocalChannel LOCAL_CHANNEL_TO_NODE_3 = new LocalChannel(CHANNEL_TO_NODE_3, PUBKEY);
}