forward.gno

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
}