package ucs03_zkgm import ( types "gno.land/p/onbloc/ibc/union/types" z "gno.land/p/onbloc/ibc/union/zkgm" u256 "gno.land/p/onbloc/math/uint256" zkgm "gno.land/r/onbloc/ibc/union/apps/ucs03_zkgm" core "gno.land/r/onbloc/ibc/union/core" ) // verifyForward checks the forwarded instruction is allowed and the timeout is set, then verifies the inner instruction. // reference: https://github.com/unionlabs/union/blob/d91c5e94354e15801bd5f82dc658eae3b79f2dad/cosmwasm/app/ucs03-zkgm/src/contract.rs#L2969-L2995 func (v *ucs03ZkgmV1) verifyForward(_ int, rlm realm, funds *funds, channelId types.ChannelId, forward z.Forward) error { if !isAllowedForwardInstruction(forward.Instruction.Opcode) { return makeError(errInvalidForwardInstruction) } if forward.TimeoutTimestamp == 0 { return makeError(errForwardZeroTimeout) } return v.verifyInternal(0, rlm, funds, channelId, forward.Path, forward.Instruction) } // executeForward builds and dispatches the next-hop packet, recording the parent for async ack settlement. // reference: https://github.com/unionlabs/union/blob/d91c5e94354e15801bd5f82dc658eae3b79f2dad/cosmwasm/app/ucs03-zkgm/src/contract.rs#L1392-L1497 func (v *ucs03ZkgmV1) executeForward(_ int, rlm realm, packet types.Packet, salt [32]byte, path *u256.Uint, forward z.Forward, intent bool) (types.RecvPacketResult, error) { // Recv bypasses verifyInternal (see imp.gno OnRecvPacket flow), // receive-side opcode check is the only enforcement point. // Do not remove it as redundant with verifyForward. if !isAllowedForwardInstruction(forward.Instruction.Opcode) { return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), makeError(errInvalidForwardInstruction) } if intent { return core.NewRecvPacketResult(types.PacketStatusSuccess, types.CloneBytes(z.ACK_ERR_ONLY_MAKER)), nil } childPacket, err := buildForwardChild(packet, path, salt, forward) if err != nil { return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), err } // Forward children build ZkgmPacket directly. Their salt is derived with // DeriveForwardSalt, not DeriveSenderSalt. Route the child through core so // it receives a normal packet commitment, then track its parent so child // resolution can write the deferred parent ack. childPacket = core.SendPacket(cross(rlm), childPacket.SourceChannelId, childPacket.TimeoutTimestamp, childPacket.Data) childHash := types.MustCommit(types.CommitPacket(childPacket)) v.store.SetInFlightPacket(0, rlm, childHash.String(), packet) zkgm.EmitForwardInFlightSet(0, rlm, childHash, types.MustCommit(types.CommitPacket(packet))) return core.NewRecvPacketResult(types.PacketStatusAsync, nil), nil } // handleForwardChild detects a forwarded child packet resolving on ack/timeout // and writes the captured parent's deferred acknowledgement through core. func (v *ucs03ZkgmV1) handleForwardChild(_ int, rlm realm, packet types.Packet, zp z.ZkgmPacket, ack []byte) bool { if !z.IsForwardedPacket(zp.Salt) { return false } key := forwardInFlightKey(packet) parent, ok := v.store.GetInFlightPacket(key) if !ok { zkgm.EmitForwardInFlightPopped(0, rlm, types.MustCommit(types.CommitPacket(packet)), false) return false } v.store.RemoveInFlightPacket(0, rlm, key) zkgm.EmitForwardInFlightPopped(0, rlm, types.MustCommit(types.CommitPacket(packet)), true) core.WriteAcknowledgement(cross(rlm), types.NewMsgWriteAcknowledgement(parent, ack)) return true } // forwardInFlightKey derives the in-flight key from the child packet's commitment hash. func forwardInFlightKey(packet types.Packet) string { return types.MustCommit(types.CommitPacket(packet)).String() } // buildForwardChild builds the next-hop packet, advancing the channel path and namespacing the salt. func buildForwardChild(packet types.Packet, path *u256.Uint, parentSalt [32]byte, forward z.Forward) (types.Packet, error) { if forward.TimeoutTimestamp == 0 { return core.NewPacket(0, 0, nil, 0), makeError(errForwardZeroTimeout) } tailPath, prevDestChannel := z.DequeueChannelFromPath(forward.Path) if prevDestChannel == 0 { return core.NewPacket(0, 0, nil, 0), makeError(errForwardMissingPrevDest) } continuationPath, nextSourceChannel := z.DequeueChannelFromPath(tailPath) if nextSourceChannel == 0 { return core.NewPacket(0, 0, nil, 0), makeError(errForwardMissingNextSource) } if packet.DestinationChannelId != types.ChannelId(prevDestChannel) { return core.NewPacket(0, 0, nil, 0), makeError(errForwardPrevDestMismatch) } nextInstruction := forward.Instruction if !continuationPath.IsZero() { nextForward := z.Forward{ Path: continuationPath, TimeoutHeight: 0, TimeoutTimestamp: forward.TimeoutTimestamp, Instruction: forward.Instruction, } forwardBytes, err := z.EncodeForward(nextForward) if err != nil { return core.NewPacket(0, 0, nil, 0), err } nextInstruction = z.Instruction{ Version: z.INSTR_VERSION_0, Opcode: z.OP_FORWARD, Operand: forwardBytes, } } intermediate, err := z.UpdateChannelPath(path, prevDestChannel) if err != nil { return core.NewPacket(0, 0, nil, 0), err } nextPath, err := z.UpdateChannelPath(intermediate, nextSourceChannel) if err != nil { return core.NewPacket(0, 0, nil, 0), err } childBytes, err := z.EncodeZkgmPacket(z.ZkgmPacket{ Salt: z.DeriveForwardSalt(parentSalt), Path: nextPath, Instruction: nextInstruction, }) if err != nil { return core.NewPacket(0, 0, nil, 0), err } sourceChannelId := types.ChannelId(nextSourceChannel) sourceChannel, err := core.GetChannel(sourceChannelId) if err != nil { return core.NewPacket(0, 0, nil, 0), err } return core.NewPacket(sourceChannelId, sourceChannel.CounterpartyChannelId, childBytes, types.Timestamp(forward.TimeoutTimestamp)), nil } // isAllowedForwardInstruction reports whether opcode may be forwarded. func isAllowedForwardInstruction(opcode uint8) bool { return opcode == z.OP_CALL || opcode == z.OP_TOKEN_ORDER || opcode == z.OP_BATCH }