htlcswitch: update link tests to be aware of fee implementation, API changes

This commit is contained in:
Olaoluwa Osuntokun 2017-06-17 00:08:19 +02:00
parent e477241de1
commit 7fc09edb76
No known key found for this signature in database
GPG Key ID: 9CC5B105D03521A2
2 changed files with 163 additions and 107 deletions

@ -92,22 +92,24 @@ func TestChannelLinkSingleHopPayment(t *testing.T) {
n.firstBobChannelLink.ChanID())) n.firstBobChannelLink.ChanID()))
} }
var amount btcutil.Amount = btcutil.SatoshiPerBitcoin
htlcAmt, totalTimelock, hops := generateHops(amount, n.firstBobChannelLink)
// Wait for: // Wait for:
// * HTLC add request to be sent to bob. // * HTLC add request to be sent to bob.
// * alice<->bob commitment state to be updated. // * alice<->bob commitment state to be updated.
// * settle request to be sent back from bob to alice. // * settle request to be sent back from bob to alice.
// * alice<->bob commitment state to be updated. // * alice<->bob commitment state to be updated.
// * user notification to be sent. // * user notification to be sent.
var amount btcutil.Amount = btcutil.SatoshiPerBitcoin invoice, err := n.makePayment(n.aliceServer, n.bobServer,
invoice, err := n.makePayment([]Peer{ n.bobServer.PubKey(), hops, amount, htlcAmt, totalTimelock)
n.aliceServer,
n.bobServer,
}, amount)
if err != nil { if err != nil {
t.Fatalf("unable to make the payment: %v", err) t.Fatalf("unable to make the payment: %v", err)
} }
// Wait for Bob to receive the revocation. // Wait for Bob to receive the revocation.
//
// TODO(roasbef); replace with select over returned err chan
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
// Check that alice invoice was settled and bandwidth of HTLC // Check that alice invoice was settled and bandwidth of HTLC
@ -153,26 +155,30 @@ func TestChannelLinkBidirectionalOneHopPayments(t *testing.T) {
n.firstBobChannelLink.ChanID())) n.firstBobChannelLink.ChanID()))
} }
const amt btcutil.Amount = 10
htlcAmt, totalTimelock, hopsForwards := generateHops(amt,
n.firstBobChannelLink)
_, _, hopsBackwards := generateHops(amt, n.aliceChannelLink)
// Send max available payment number in both sides, thereby testing // Send max available payment number in both sides, thereby testing
// the property of channel link to cope with overflowing. // the property of channel link to cope with overflowing.
errChan := make(chan error) errChan := make(chan error)
count := 2 * lnwallet.MaxHTLCNumber count := 2 * lnwallet.MaxHTLCNumber
for i := 0; i < count/2; i++ { for i := 0; i < count/2; i++ {
go func() { go func() {
_, err := n.makePayment([]Peer{ _, err := n.makePayment(n.aliceServer, n.bobServer,
n.aliceServer, n.bobServer.PubKey(), hopsForwards, amt, htlcAmt,
n.bobServer, totalTimelock)
}, 10)
errChan <- err errChan <- err
}() }()
} }
for i := 0; i < count/2; i++ { for i := 0; i < count/2; i++ {
go func() { go func() {
_, err := n.makePayment([]Peer{ _, err := n.makePayment(n.bobServer, n.aliceServer,
n.bobServer, n.aliceServer.PubKey(), hopsBackwards, amt, htlcAmt,
n.aliceServer, totalTimelock)
}, 10)
errChan <- err errChan <- err
}() }()
} }
@ -240,6 +246,10 @@ func TestChannelLinkMultiHopPayment(t *testing.T) {
n.carolChannelLink.ChanID())) n.carolChannelLink.ChanID()))
} }
var amount btcutil.Amount = btcutil.SatoshiPerBitcoin
htlcAmt, totalTimelock, hops := generateHops(amount,
n.firstBobChannelLink, n.carolChannelLink)
// Wait for: // Wait for:
// * HTLC add request to be sent from Alice to Bob. // * HTLC add request to be sent from Alice to Bob.
// * Alice<->Bob commitment states to be updated. // * Alice<->Bob commitment states to be updated.
@ -250,12 +260,9 @@ func TestChannelLinkMultiHopPayment(t *testing.T) {
// * settle request to be sent back from Bob to Alice. // * settle request to be sent back from Bob to Alice.
// * Alice<->Bob commitment states to be updated. // * Alice<->Bob commitment states to be updated.
// * user notification to be sent. // * user notification to be sent.
var amount btcutil.Amount = btcutil.SatoshiPerBitcoin invoice, err := n.makePayment(n.aliceServer, n.carolServer,
invoice, err := n.makePayment([]Peer{ n.bobServer.PubKey(), hops, amount, htlcAmt,
n.aliceServer, totalTimelock)
n.bobServer,
n.carolServer,
}, amount)
if err != nil { if err != nil {
t.Fatalf("unable to send payment: %v", err) t.Fatalf("unable to send payment: %v", err)
} }
@ -269,24 +276,31 @@ func TestChannelLinkMultiHopPayment(t *testing.T) {
t.Fatal("alice invoice wasn't settled") t.Fatal("alice invoice wasn't settled")
} }
if aliceBandwidthBefore-amount != n.aliceChannelLink.Bandwidth() { expectedAliceBandwidth := aliceBandwidthBefore - htlcAmt
t.Fatal("the bandwidth of alice channel link which handles " + if expectedAliceBandwidth != n.aliceChannelLink.Bandwidth() {
"alice->bob channel wasn't decreased on htlc amount") t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedAliceBandwidth, n.aliceChannelLink.Bandwidth())
} }
if firstBobBandwidthBefore+amount != n.firstBobChannelLink.Bandwidth() { expectedBobBandwidth1 := firstBobBandwidthBefore + htlcAmt
t.Fatal("the bandwidth of bob channel link which handles " + if expectedBobBandwidth1 != n.firstBobChannelLink.Bandwidth() {
"alice->bob channel wasn't increased on htlc amount") t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedBobBandwidth1, n.firstBobChannelLink.Bandwidth())
} }
if secondBobBandwidthBefore-amount != n.secondBobChannelLink.Bandwidth() { expectedBobBandwidth2 := secondBobBandwidthBefore - amount
t.Fatal("the bandwidth of bob channel link which handles " + if expectedBobBandwidth2 != n.secondBobChannelLink.Bandwidth() {
"bob->carol channel wasn't decreased on htlc amount") t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedBobBandwidth2, n.secondBobChannelLink.Bandwidth())
} }
if carolBandwidthBefore+amount != n.carolChannelLink.Bandwidth() { expectedCarolBandwidth := carolBandwidthBefore + amount
t.Fatal("the bandwidth of carol channel link which handles " + if expectedCarolBandwidth != n.carolChannelLink.Bandwidth() {
"carol->bob channel wasn't decreased on htlc amount") t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedCarolBandwidth, n.carolChannelLink.Bandwidth())
}
}
} }
} }
@ -308,18 +322,18 @@ func TestChannelLinkMultiHopInsufficientPayment(t *testing.T) {
secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth() secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth()
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth() aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
var amount btcutil.Amount = 4 * btcutil.SatoshiPerBitcoin
htlcAmt, totalTimelock, hops := generateHops(amount,
n.firstBobChannelLink, n.carolChannelLink)
// Wait for: // Wait for:
// * HTLC add request to be sent to from Alice to Bob. // * HTLC add request to be sent to from Alice to Bob.
// * Alice<->Bob commitment states to be updated. // * Alice<->Bob commitment states to be updated.
// * Bob trying to add HTLC add request in Bob<->Carol channel. // * Bob trying to add HTLC add request in Bob<->Carol channel.
// * Cancel HTLC request to be sent back from Bob to Alice. // * Cancel HTLC request to be sent back from Bob to Alice.
// * user notification to be sent. // * user notification to be sent.
var amount btcutil.Amount = 4 * btcutil.SatoshiPerBitcoin invoice, err := n.makePayment(n.aliceServer, n.bobServer,
invoice, err := n.makePayment([]Peer{ n.bobServer.PubKey(), hops, amount, htlcAmt, totalTimelock)
n.aliceServer,
n.bobServer,
n.carolServer,
}, amount)
if err == nil { if err == nil {
t.Fatal("error haven't been received") t.Fatal("error haven't been received")
} else if err.Error() != errors.New(lnwire.InsufficientCapacity).Error() { } else if err.Error() != errors.New(lnwire.InsufficientCapacity).Error() {
@ -354,7 +368,6 @@ func TestChannelLinkMultiHopInsufficientPayment(t *testing.T) {
t.Fatal("the bandwidth of carol channel link which handles " + t.Fatal("the bandwidth of carol channel link which handles " +
"bob->carol channel should be the same") "bob->carol channel should be the same")
} }
} }
// TestChannelLinkMultiHopUnknownPaymentHash checks that we receive remote error // TestChannelLinkMultiHopUnknownPaymentHash checks that we receive remote error
@ -376,19 +389,16 @@ func TestChannelLinkMultiHopUnknownPaymentHash(t *testing.T) {
var amount btcutil.Amount = btcutil.SatoshiPerBitcoin var amount btcutil.Amount = btcutil.SatoshiPerBitcoin
// Generate route convert it to blob, and return next destination for htlcAmt, totalTimelock, hops := generateHops(amount,
// htlc add request. n.firstBobChannelLink, n.carolChannelLink)
peers := []Peer{ blob, err := generateRoute(hops...)
n.bobServer,
n.carolServer,
}
firstNode, blob, err := generateRoute(peers)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Generate payment: invoice and htlc. // Generate payment: invoice and htlc.
invoice, htlc, err := generatePayment(amount, blob) invoice, htlc, err := generatePayment(amount, htlcAmt, totalTimelock,
blob)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -403,8 +413,8 @@ func TestChannelLinkMultiHopUnknownPaymentHash(t *testing.T) {
} }
// Send payment and expose err channel. // Send payment and expose err channel.
if _, err := n.aliceServer.htlcSwitch.SendHTLC(firstNode, _, err = n.aliceServer.htlcSwitch.SendHTLC(n.bobServer.PubKey(), htlc)
htlc); err == nil { if err == nil {
t.Fatal("error wasn't received") t.Fatal("error wasn't received")
} }
@ -441,7 +451,7 @@ func TestChannelLinkMultiHopUnknownPaymentHash(t *testing.T) {
// TestChannelLinkMultiHopUnknownNextHop construct the chain of hops // TestChannelLinkMultiHopUnknownNextHop construct the chain of hops
// Carol<->Bob<->Alice and checks that we receive remote error from Bob if he // Carol<->Bob<->Alice and checks that we receive remote error from Bob if he
// has no idea about next hop (hop might goes down and routing info not updated // has no idea about next hop (hop might goes down and routing info not updated
// yet) // yet).
func TestChannelLinkMultiHopUnknownNextHop(t *testing.T) { func TestChannelLinkMultiHopUnknownNextHop(t *testing.T) {
n := newThreeHopNetwork(t, n := newThreeHopNetwork(t,
btcutil.SatoshiPerBitcoin*3, btcutil.SatoshiPerBitcoin*3,
@ -458,13 +468,12 @@ func TestChannelLinkMultiHopUnknownNextHop(t *testing.T) {
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth() aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
var amount btcutil.Amount = btcutil.SatoshiPerBitcoin var amount btcutil.Amount = btcutil.SatoshiPerBitcoin
htlcAmt, totalTimelock, hops := generateHops(amount,
n.firstBobChannelLink, n.carolChannelLink)
dave := newMockServer(t, "save") davePub := newMockServer(t, "save").PubKey()
invoice, err := n.makePayment([]Peer{ invoice, err := n.makePayment(n.aliceServer, n.bobServer, davePub, hops,
n.aliceServer, amount, htlcAmt, totalTimelock)
n.bobServer,
dave,
}, amount)
if err == nil { if err == nil {
t.Fatal("error haven't been received") t.Fatal("error haven't been received")
} else if err.Error() != errors.New(lnwire.UnknownDestination).Error() { } else if err.Error() != errors.New(lnwire.UnknownDestination).Error() {
@ -472,6 +481,8 @@ func TestChannelLinkMultiHopUnknownNextHop(t *testing.T) {
} }
// Wait for Alice to receive the revocation. // Wait for Alice to receive the revocation.
//
// TODO(roasbeef): add in ntfn hook for state transition completion
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
// Check that alice invoice wasn't settled and bandwidth of htlc // Check that alice invoice wasn't settled and bandwidth of htlc
@ -525,11 +536,11 @@ func TestChannelLinkMultiHopDecodeError(t *testing.T) {
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth() aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
var amount btcutil.Amount = btcutil.SatoshiPerBitcoin var amount btcutil.Amount = btcutil.SatoshiPerBitcoin
invoice, err := n.makePayment([]Peer{ htlcAmt, totalTimelock, hops := generateHops(amount,
n.aliceServer, n.firstBobChannelLink, n.carolChannelLink)
n.bobServer,
n.carolServer, invoice, err := n.makePayment(n.aliceServer, n.carolServer,
}, amount) n.bobServer.PubKey(), hops, amount, htlcAmt, totalTimelock)
if err == nil { if err == nil {
t.Fatal("error haven't been received") t.Fatal("error haven't been received")
} else if err.Error() != errors.New(lnwire.SphinxParseError).Error() { } else if err.Error() != errors.New(lnwire.SphinxParseError).Error() {
@ -624,7 +635,7 @@ func TestChannelLinkSingleHopMessageOrdering(t *testing.T) {
n.aliceServer.record(func(m lnwire.Message) { n.aliceServer.record(func(m lnwire.Message) {
if getChanID(m) == chanPoint { if getChanID(m) == chanPoint {
if len(aliceOrder) == 0 { if len(aliceOrder) == 0 {
t.Fatal("redudant messages") t.Fatal("redundant messages")
} }
if reflect.TypeOf(aliceOrder[0]) != reflect.TypeOf(m) { if reflect.TypeOf(aliceOrder[0]) != reflect.TypeOf(m) {
@ -640,7 +651,7 @@ func TestChannelLinkSingleHopMessageOrdering(t *testing.T) {
n.bobServer.record(func(m lnwire.Message) { n.bobServer.record(func(m lnwire.Message) {
if getChanID(m) == chanPoint { if getChanID(m) == chanPoint {
if len(bobOrder) == 0 { if len(bobOrder) == 0 {
t.Fatal("redudant messages") t.Fatal("redundant messages")
} }
if reflect.TypeOf(bobOrder[0]) != reflect.TypeOf(m) { if reflect.TypeOf(bobOrder[0]) != reflect.TypeOf(m) {
@ -657,16 +668,17 @@ func TestChannelLinkSingleHopMessageOrdering(t *testing.T) {
} }
defer n.stop() defer n.stop()
var amount btcutil.Amount = btcutil.SatoshiPerBitcoin
htlcAmt, totalTimelock, hops := generateHops(amount, n.firstBobChannelLink)
// Wait for: // Wait for:
// * htlc add htlc request to be sent to alice // * htlc add htlc request to be sent to alice
// * alice<->bob commitment state to be updated // * alice<->bob commitment state to be updated
// * settle request to be sent back from alice to bob // * settle request to be sent back from alice to bob
// * alice<->bob commitment state to be updated // * alice<->bob commitment state to be updated
var amount btcutil.Amount = btcutil.SatoshiPerBitcoin _, err := n.makePayment(n.aliceServer, n.bobServer,
if _, err := n.makePayment([]Peer{ n.bobServer.PubKey(), hops, amount, htlcAmt, totalTimelock)
n.aliceServer, if err != nil {
n.bobServer,
}, amount); err != nil {
t.Fatalf("unable to make the payment: %v", err) t.Fatalf("unable to make the payment: %v", err)
} }
} }

@ -34,7 +34,7 @@ type mockServer struct {
name string name string
messages chan lnwire.Message messages chan lnwire.Message
id []byte id [33]byte
htlcSwitch *Switch htlcSwitch *Switch
registry *mockInvoiceRegistry registry *mockInvoiceRegistry
@ -44,9 +44,13 @@ type mockServer struct {
var _ Peer = (*mockServer)(nil) var _ Peer = (*mockServer)(nil)
func newMockServer(t *testing.T, name string) *mockServer { func newMockServer(t *testing.T, name string) *mockServer {
var id [33]byte
h := sha256.Sum256([]byte(name))
copy(id[:], h[:])
return &mockServer{ return &mockServer{
t: t, t: t,
id: []byte(name), id: id,
name: name, name: name,
messages: make(chan lnwire.Message, 3000), messages: make(chan lnwire.Message, 3000),
quit: make(chan bool), quit: make(chan bool),
@ -91,24 +95,20 @@ func (s *mockServer) Start() error {
// mockHopIterator represents the test version of hop iterator which instead // mockHopIterator represents the test version of hop iterator which instead
// of encrypting the path in onion blob just stores the path as a list of hops. // of encrypting the path in onion blob just stores the path as a list of hops.
type mockHopIterator struct { type mockHopIterator struct {
hops []HopID hops []ForwardingInfo
} }
func newMockHopIterator(hops ...HopID) HopIterator { func newMockHopIterator(hops ...ForwardingInfo) HopIterator {
return &mockHopIterator{hops: hops} return &mockHopIterator{hops: hops}
} }
func (r *mockHopIterator) Next() *HopID { func (r *mockHopIterator) ForwardingInstructions() ForwardingInfo {
if len(r.hops) != 0 { h := r.hops[0]
next := r.hops[0] r.hops = r.hops[1:]
r.hops = r.hops[1:] return h
return &next
}
return nil
} }
func (r *mockHopIterator) Encode(w io.Writer) error { func (r *mockHopIterator) EncodeNextHop(w io.Writer) error {
var hopLength [4]byte var hopLength [4]byte
binary.BigEndian.PutUint32(hopLength[:], uint32(len(r.hops))) binary.BigEndian.PutUint32(hopLength[:], uint32(len(r.hops)))
@ -117,7 +117,7 @@ func (r *mockHopIterator) Encode(w io.Writer) error {
} }
for _, hop := range r.hops { for _, hop := range r.hops {
if _, err := w.Write(hop[:]); err != nil { if err := hop.encode(w); err != nil {
return err return err
} }
} }
@ -125,15 +125,33 @@ func (r *mockHopIterator) Encode(w io.Writer) error {
return nil return nil
} }
func (f *ForwardingInfo) encode(w io.Writer) error {
if _, err := w.Write([]byte{byte(f.Network)}); err != nil {
return err
}
if err := binary.Write(w, binary.BigEndian, f.NextHop); err != nil {
return err
}
if err := binary.Write(w, binary.BigEndian, f.AmountToForward); err != nil {
return err
}
if err := binary.Write(w, binary.BigEndian, f.OutgoingCTLV); err != nil {
return err
}
return nil
}
var _ HopIterator = (*mockHopIterator)(nil) var _ HopIterator = (*mockHopIterator)(nil)
// mockIteratorDecoder test version of hop iterator decoder which decodes the // mockIteratorDecoder test version of hop iterator decoder which decodes the
// encoded array of hops. // encoded array of hops.
type mockIteratorDecoder struct{} type mockIteratorDecoder struct{}
func (p *mockIteratorDecoder) Decode(r io.Reader, meta []byte) ( func (p *mockIteratorDecoder) Decode(r io.Reader, meta []byte) (HopIterator, error) {
HopIterator, error) {
var b [4]byte var b [4]byte
_, err := r.Read(b[:]) _, err := r.Read(b[:])
if err != nil { if err != nil {
@ -141,21 +159,41 @@ func (p *mockIteratorDecoder) Decode(r io.Reader, meta []byte) (
} }
hopLength := binary.BigEndian.Uint32(b[:]) hopLength := binary.BigEndian.Uint32(b[:])
hops := make([]HopID, hopLength) hops := make([]ForwardingInfo, hopLength)
for i := uint32(0); i < hopLength; i++ { for i := uint32(0); i < hopLength; i++ {
var hop HopID f := &ForwardingInfo{}
if err := f.decode(r); err != nil {
_, err := r.Read(hop[:])
if err != nil {
return nil, err return nil, err
} }
hops[i] = hop hops[i] = *f
} }
return newMockHopIterator(hops...), nil return newMockHopIterator(hops...), nil
} }
func (f *ForwardingInfo) decode(r io.Reader) error {
var net [1]byte
if _, err := r.Read(net[:]); err != nil {
return err
}
f.Network = NetworkHop(net[0])
if err := binary.Read(r, binary.BigEndian, &f.NextHop); err != nil {
return err
}
if err := binary.Read(r, binary.BigEndian, &f.AmountToForward); err != nil {
return err
}
if err := binary.Read(r, binary.BigEndian, &f.OutgoingCTLV); err != nil {
return err
}
return nil
}
// messageInterceptor is function that handles the incoming peer messages and // messageInterceptor is function that handles the incoming peer messages and
// may decide should we handle it or not. // may decide should we handle it or not.
type messageInterceptor func(m lnwire.Message) type messageInterceptor func(m lnwire.Message)
@ -218,11 +256,7 @@ func (s *mockServer) readHandler(message lnwire.Message) error {
return nil return nil
} }
func (s *mockServer) ID() [sha256.Size]byte { func (s *mockServer) PubKey() [33]byte {
return [sha256.Size]byte{}
}
func (s *mockServer) PubKey() []byte {
return s.id return s.id
} }
@ -247,21 +281,27 @@ func (s *mockServer) Stop() {
} }
func (s *mockServer) String() string { func (s *mockServer) String() string {
return string(s.id) return s.name
} }
type mockChannelLink struct { type mockChannelLink struct {
chanID lnwire.ChannelID shortChanID lnwire.ShortChannelID
peer Peer
chanID lnwire.ChannelID
peer Peer
packets chan *htlcPacket packets chan *htlcPacket
} }
func newMockChannelLink(chanID lnwire.ChannelID, func newMockChannelLink(chanID lnwire.ChannelID, shortChanID lnwire.ShortChannelID,
peer Peer) *mockChannelLink { peer Peer) *mockChannelLink {
return &mockChannelLink{ return &mockChannelLink{
chanID: chanID, chanID: chanID,
packets: make(chan *htlcPacket, 1), shortChanID: shortChanID,
peer: peer, packets: make(chan *htlcPacket, 1),
peer: peer,
} }
} }
@ -272,15 +312,19 @@ func (f *mockChannelLink) HandleSwitchPacket(packet *htlcPacket) {
func (f *mockChannelLink) HandleChannelUpdate(lnwire.Message) { func (f *mockChannelLink) HandleChannelUpdate(lnwire.Message) {
} }
func (f *mockChannelLink) UpdateForwardingPolicy(_ ForwardingPolicy) {
}
func (f *mockChannelLink) Stats() (uint64, btcutil.Amount, btcutil.Amount) { func (f *mockChannelLink) Stats() (uint64, btcutil.Amount, btcutil.Amount) {
return 0, 0, 0 return 0, 0, 0
} }
func (f *mockChannelLink) ChanID() lnwire.ChannelID { return f.chanID } func (f *mockChannelLink) ChanID() lnwire.ChannelID { return f.chanID }
func (f *mockChannelLink) Bandwidth() btcutil.Amount { return 99999999 } func (f *mockChannelLink) ShortChanID() lnwire.ShortChannelID { return f.shortChanID }
func (f *mockChannelLink) Peer() Peer { return f.peer } func (f *mockChannelLink) Bandwidth() btcutil.Amount { return 99999999 }
func (f *mockChannelLink) Start() error { return nil } func (f *mockChannelLink) Peer() Peer { return f.peer }
func (f *mockChannelLink) Stop() {} func (f *mockChannelLink) Start() error { return nil }
func (f *mockChannelLink) Stop() {}
var _ ChannelLink = (*mockChannelLink)(nil) var _ ChannelLink = (*mockChannelLink)(nil)