package core import ( "bytes" "chain" "encoding/hex" "time" "gno.land/p/aib/ibc/host" "gno.land/p/aib/ibc/lightclient" "gno.land/p/aib/ibc/types" "gno.land/p/nt/ufmt/v0" ) const maxTimeoutDelta time.Duration = 24 * time.Hour func SendPacket(cur realm, msg types.MsgSendPacket) (sequence uint64) { if err := msg.ValidateBasic(); err != nil { panic(err) } clientID := msg.SourceClient c := store.getClient(clientID) if c == nil { panic(ufmt.Sprintf("client %s not found", clientID)) } if status := c.lightClient.Status(); status != lightclient.Active { panic(ufmt.Sprintf("client (%s) status is %s", clientID, status)) } // Ensure counterparty has been registered if c.counterpartyClientID == "" { panic(ufmt.Sprintf("counterparty not found for client %s", clientID)) } // timeoutTimestamp must be greater than current block time and less or equal // to current block time + maxTimeoutDelta. var ( timeout = time.Unix(int64(msg.TimeoutTimestamp), 0) minTimeout = time.Now() maxTimeout = minTimeout.Add(maxTimeoutDelta) ) if !timeout.After(minTimeout) { panic(ufmt.Sprintf( "timeout is less than or equal the current block timestamp, %d <= %d", msg.TimeoutTimestamp, minTimeout.Unix(), )) } if timeout.After(maxTimeout) { panic(ufmt.Sprintf( "timeout is after the max allowed timeout, %d > %d", msg.TimeoutTimestamp, maxTimeout.Unix(), )) } // check if the latest consensus timestamp is lower than timeoutTimestamp latestTimestamp, err := c.lightClient.TimestampAtHeight(c.lightClient.LatestHeight()) if err != nil { panic(err) } if latestTimestamp >= msg.TimeoutTimestamp { panic(ufmt.Errorf("latest timestamp: %d, timeout timestamp: %d", latestTimestamp, msg.TimeoutTimestamp)) } // construct packet from given fields sequence = uint64(c.sendSeq.Next()) packet := types.NewPacket(sequence, msg.SourceClient, c.counterpartyClientID, msg.TimeoutTimestamp, msg.Payloads...) if err := packet.ValidateBasic(); err != nil { panic(ufmt.Errorf("constructed packet failed basic validation: %v", err)) } // set the packet commitment c.setPacketCommitment(sequence, packet) // emit events chain.Emit(types.EventTypeSendPacket, types.AttributeKeySrcClient, packet.SourceClient, types.AttributeKeyDstClient, packet.DestinationClient, types.AttributeKeySequence, ufmt.Sprintf("%d", packet.Sequence), types.AttributeKeyTimeoutTimestamp, ufmt.Sprintf("%d", packet.TimeoutTimestamp), types.AttributeKeyEncodedPacketHex, hex.EncodeToString(packet.ProtoMarshal()), ) // Invoke registed app OnSendPacket() for each payload.SourcePort. for i, payload := range msg.Payloads { app := store.route(payload.SourcePort) err := app.OnSendPacket( cross(cur), msg.SourceClient, c.counterpartyClientID, sequence, payload, ) if err != nil { panic(ufmt.Sprintf( "send packet failed for payload #%d app %q: %v", i, payload.SourcePort, err, )) } } return sequence } func RecvPacket(cur realm, msg types.MsgRecvPacket) types.ResponseResultType { if err := msg.ValidateBasic(); err != nil { panic(err) } clientID := msg.Packet.DestinationClient c := store.getClient(clientID) if c == nil { panic(ufmt.Sprintf("client %s not found", clientID)) } ensureAuthorizedRelayer() // check client is active if status := c.lightClient.Status(); status != lightclient.Active { panic(ufmt.Sprintf("client (%s) status is %s", clientID, status)) } // check counterparty if c.counterpartyClientID != msg.Packet.SourceClient { panic(ufmt.Sprintf( "counterparty id (%s) does not match packet source id (%s)", c.counterpartyClientID, msg.Packet.SourceClient, )) } currentTimestamp := uint64(time.Now().Unix()) if currentTimestamp >= msg.Packet.TimeoutTimestamp { panic(ufmt.Sprintf( "current timestamp: %d, timeout timestamp: %d", currentTimestamp, msg.Packet.TimeoutTimestamp, )) } // REPLAY PROTECTION: Packet receipts will indicate that a packet has already // been received. // Packet receipts must not be pruned, unless it has been marked stale by the // increase of the recvStartSequence. TODO check relevancy of comment if c.hasPacketReceipt(msg.Packet.Sequence) { // This error indicates that the packet has already been relayed. Core IBC // will treat this error as a no-op in order to prevent an entire relay // transaction from failing and consuming unnecessary fees. return types.RESPONSE_NOOP } // Verify existence of the commitment bytes in the proofs. var ( key = host.PacketCommitmentKey(msg.Packet.SourceClient, msg.Packet.Sequence) merklePath = types.BuildMerklePath(c.counterpartyMerklePrefix, key) value = types.CommitPacket(msg.Packet) ) if err := c.lightClient.VerifyMembership( msg.ProofHeight, msg.ProofCommitment, merklePath, value, ); err != nil { panic(ufmt.Sprintf( "failed packet commitment verification for client (%s): %v", clientID, err, )) } // Set Packet Receipt to prevent timeout from occurring on counterparty c.setPacketReceipt(msg.Packet.Sequence) // Emit events chain.Emit(types.EventTypeRecvPacket, types.AttributeKeySrcClient, msg.Packet.SourceClient, types.AttributeKeyDstClient, msg.Packet.DestinationClient, types.AttributeKeySequence, ufmt.Sprintf("%d", msg.Packet.Sequence), types.AttributeKeyTimeoutTimestamp, ufmt.Sprintf("%d", msg.Packet.TimeoutTimestamp), types.AttributeKeyEncodedPacketHex, hex.EncodeToString(msg.Packet.ProtoMarshal()), ) c.writeRecvPacketAcknowledgement(0, cur, msg.Packet) return types.RESPONSE_SUCCESS } // writeRecvPacketAcknowledgement is a non-crossing helper: rlm is the caller's // live cur, threaded as a non-first param via the `_ int, rlm realm` shape so // the cross to each app's OnRecvPacket can be issued from RecvPacket's frame. func (c *client) writeRecvPacketAcknowledgement(_ int, rlm realm, packet types.Packet) { var ( // build up the recv results for each application callback. ack = types.Acknowledgement{ AppAcknowledgements: [][]byte{}, } isSuccess = true ) // Invoke registed app OnRecvPacket() for each payload.DestinationPort. for i, payload := range packet.Payloads { app := store.route(payload.DestinationPort) res := app.OnRecvPacket( cross(rlm), packet.SourceClient, packet.DestinationClient, packet.Sequence, payload, ) if res.Status == types.PacketStatus_Failure { isSuccess = false // construct acknowledgement with single app acknowledgement that is the // sentinel error acknowledgement ack = types.Acknowledgement{ AppAcknowledgements: [][]byte{types.UniversalErrorAcknowledgement()}, } break } if res.Status == types.PacketStatus_Async { if len(packet.Payloads) > 1 { panic("async ack not supported for multi-payload packets") } c.savePendingAsyncAck(packet, app.pkgPath) return } // successful app acknowledgement cannot equal sentinel error // acknowledgement if bytes.Equal(res.Acknowledgement, types.UniversalErrorAcknowledgement()) { panic(ufmt.Sprintf( "callback error for payload #%d app %q: application acknowledgement cannot be sentinel error acknowledgement", i, payload.DestinationPort, )) } // append app acknowledgement to the overall acknowledgement ack.AppAcknowledgements = append(ack.AppAcknowledgements, res.Acknowledgement) } // Sanity check to ensure returned acknowledgement and calculated isSuccess // boolean matches if ack.Success() != isSuccess { panic("acknowledgement success flag mismatch") } if err := ack.Validate(); err != nil { panic(err) } // set the acknowledgement so that it can be verified on the other side c.setPacketAcknowledgement(packet.Sequence, types.CommitAcknowledgement(ack)) emitWriteAcknowledgement(packet, ack) } func WriteAcknowledgement(cur realm, clientID string, sequence uint64, ack types.Acknowledgement) { c := store.getClient(clientID) if c == nil { panic(ufmt.Sprintf("client %s not found", clientID)) } pending, found := c.getPendingAsyncAck(sequence) if !found { panic(ufmt.Sprintf("no pending async ack for client=%s sequence=%d", clientID, sequence)) } callerPath := cur.Previous().PkgPath() if callerPath != pending.appPkgPath { panic(ufmt.Sprintf( "caller %s is not authorized to write ack for sequence %d (expected %s)", callerPath, sequence, pending.appPkgPath, )) } if c.hasPacketAcknowledgement(sequence) { panic(ufmt.Sprintf("acknowledgement already written for sequence %d", sequence)) } if err := ack.Validate(); err != nil { panic(err) } c.setPacketAcknowledgement(sequence, types.CommitAcknowledgement(ack)) c.deletePendingAsyncAck(sequence) emitWriteAcknowledgement(pending.packet, ack) } func emitWriteAcknowledgement(packet types.Packet, ack types.Acknowledgement) { chain.Emit(types.EventTypeWriteAck, types.AttributeKeySrcClient, packet.SourceClient, types.AttributeKeyDstClient, packet.DestinationClient, types.AttributeKeySequence, ufmt.Sprintf("%d", packet.Sequence), types.AttributeKeyTimeoutTimestamp, ufmt.Sprintf("%d", packet.TimeoutTimestamp), types.AttributeKeyEncodedPacketHex, hex.EncodeToString(packet.ProtoMarshal()), types.AttributeKeyEncodedAckHex, hex.EncodeToString(ack.ProtoMarshal()), ) } func Acknowledgement(cur realm, msg types.MsgAcknowledgement) types.ResponseResultType { if err := msg.ValidateBasic(); err != nil { panic(err) } clientID := msg.Packet.SourceClient c := store.getClient(clientID) if c == nil { panic(ufmt.Sprintf("client %s not found", clientID)) } ensureAuthorizedRelayer() // check client is active if status := c.lightClient.Status(); status != lightclient.Active { panic(ufmt.Sprintf("client (%s) status is %s", clientID, status)) } // check counterparty if c.counterpartyClientID != msg.Packet.DestinationClient { panic(ufmt.Sprintf( "counterparty id (%s) does not match packet destination id (%s)", c.counterpartyClientID, msg.Packet.DestinationClient, )) } commitment := c.getPacketCommitment(msg.Packet.Sequence) if len(commitment) == 0 { // This error indicates that the acknowledgement has already been relayed // or there is a misconfigured relayer attempting to prove an // acknowledgement for a packet never sent. Core IBC will treat this error // as a no-op in order to prevent an entire relay transaction from failing // and consuming unnecessary fees. return types.RESPONSE_NOOP } packetCommitment := types.CommitPacket(msg.Packet) // ensure integrity of commitment if !bytes.Equal(commitment, packetCommitment) { h1, h2 := hex.EncodeToString(packetCommitment), hex.EncodeToString(commitment) panic(ufmt.Sprintf( "commitment bytes are not equal: got (%v), expected (%v)", h1, h2, )) } // Verify existence of the acknowledgement commitment bytes in the proofs. var ( key = host.PacketAcknowledgementKey(msg.Packet.DestinationClient, msg.Packet.Sequence) merklePath = types.BuildMerklePath(c.counterpartyMerklePrefix, key) value = types.CommitAcknowledgement(msg.Acknowledgement) ) if err := c.lightClient.VerifyMembership( msg.ProofHeight, msg.ProofAcked, merklePath, value, ); err != nil { panic(ufmt.Sprintf( "failed packet acknowledgement verification for client (%s): %v", clientID, err, )) } c.deletePacketCommitment(msg.Packet.Sequence) chain.Emit(types.EventTypeAcknowledgePacket, types.AttributeKeySrcClient, msg.Packet.SourceClient, types.AttributeKeyDstClient, msg.Packet.DestinationClient, types.AttributeKeySequence, ufmt.Sprintf("%d", msg.Packet.Sequence), types.AttributeKeyTimeoutTimestamp, ufmt.Sprintf("%d", msg.Packet.TimeoutTimestamp), types.AttributeKeyEncodedPacketHex, hex.EncodeToString(msg.Packet.ProtoMarshal()), ) // Invoke registed app OnAcknowledgementPacket() for each payload.SourcePort. recvSuccess := !bytes.Equal(msg.Acknowledgement.AppAcknowledgements[0], types.UniversalErrorAcknowledgement()) for i, payload := range msg.Packet.Payloads { app := store.route(payload.SourcePort) // if recv was successful, each payload should have its own acknowledgement // so we send each individual acknowledgment to the application otherwise, // the acknowledgement only contains the sentinel error acknowledgement // which we send to the application. The application is responsible for // knowing that this is an error acknowledgement and executing the // appropriate logic. var ack []byte if recvSuccess { ack = msg.Acknowledgement.AppAcknowledgements[i] } else { ack = types.UniversalErrorAcknowledgement() } err := app.OnAcknowledgementPacket( cross(cur), msg.Packet.SourceClient, msg.Packet.DestinationClient, msg.Packet.Sequence, ack, payload, ) if err != nil { panic(ufmt.Sprintf( "acknowledgement packet failed for payload #%d app %q: %v", i, payload.SourcePort, err, )) } } return types.RESPONSE_SUCCESS } func Timeout(cur realm, msg types.MsgTimeout) types.ResponseResultType { if err := msg.ValidateBasic(); err != nil { panic(err) } clientID := msg.Packet.SourceClient c := store.getClient(clientID) if c == nil { panic(ufmt.Sprintf("client %s not found", clientID)) } ensureAuthorizedRelayer() // check client is active if status := c.lightClient.Status(); status != lightclient.Active { panic(ufmt.Sprintf("client (%s) status is %s", clientID, status)) } // check counterparty if c.counterpartyClientID != msg.Packet.DestinationClient { panic(ufmt.Sprintf( "counterparty id (%s) does not match packet destination id (%s)", c.counterpartyClientID, msg.Packet.DestinationClient, )) } // check that timeout timestamp has passed on the other end proofTimestamp, err := c.lightClient.TimestampAtHeight(msg.ProofHeight) if err != nil { panic(err) } if proofTimestamp < msg.Packet.TimeoutTimestamp { panic(ufmt.Errorf("proof timestamp: %d, timeout timestamp: %d", proofTimestamp, msg.Packet.TimeoutTimestamp)) } commitment := c.getPacketCommitment(msg.Packet.Sequence) if len(commitment) == 0 { // This error indicates that the timeout has already been relayed or there // is a misconfigured relayer attempting to prove a timeout for a packet // never sent. Core IBC will treat this error as a no-op in order to // prevent an entire relay transaction from failing and consuming // unnecessary fees. return types.RESPONSE_NOOP } packetCommitment := types.CommitPacket(msg.Packet) // ensure integrity of commitment if !bytes.Equal(commitment, packetCommitment) { h1, h2 := hex.EncodeToString(packetCommitment), hex.EncodeToString(commitment) panic(ufmt.Sprintf( "commitment bytes are not equal: got (%v), expected (%v)", h1, h2, )) } // Verify packet receipt absence var ( key = host.PacketReceiptKey(msg.Packet.DestinationClient, msg.Packet.Sequence) merklePath = types.BuildMerklePath(c.counterpartyMerklePrefix, key) ) if err := c.lightClient.VerifyNonMembership( msg.ProofHeight, msg.ProofUnreceived, merklePath, ); err != nil { panic(ufmt.Sprintf( "failed packet receipt absence verification for client (%s): %v", clientID, err, )) } c.deletePacketCommitment(msg.Packet.Sequence) chain.Emit(types.EventTypeTimeoutPacket, types.AttributeKeySrcClient, msg.Packet.SourceClient, types.AttributeKeyDstClient, msg.Packet.DestinationClient, types.AttributeKeySequence, ufmt.Sprintf("%d", msg.Packet.Sequence), types.AttributeKeyTimeoutTimestamp, ufmt.Sprintf("%d", msg.Packet.TimeoutTimestamp), types.AttributeKeyEncodedPacketHex, hex.EncodeToString(msg.Packet.ProtoMarshal()), ) for i, payload := range msg.Packet.Payloads { app := store.route(payload.SourcePort) err := app.OnTimeoutPacket( cross(cur), msg.Packet.SourceClient, msg.Packet.DestinationClient, msg.Packet.Sequence, payload, ) if err != nil { panic(ufmt.Sprintf( "timeout packet failed for payload #%d app %q: %v", i, payload.SourcePort, err, )) } } return types.RESPONSE_SUCCESS }