package sphinx

import (
	"bytes"
	"crypto/ecdsa"
	"crypto/hmac"
	"crypto/sha256"
	"fmt"
	"io"
	"math/big"

	"github.com/btcsuite/btcd/btcec"
	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcutil"
)

const (
	// addressSize is the length of the serialized address used to uniquely
	// identify the next hop to forward the onion to. BOLT 04 defines this
	// as 8 byte channel_id.
	AddressSize = 8

	// RealmByteSize is the number of bytes that the realm byte occupies.
	RealmByteSize = 1

	// AmtForwardSize is the number of bytes that the amount to forward
	// occupies.
	AmtForwardSize = 8

	// OutgoingCLTVSize is the number of bytes that the outgoing CLTV value
	// occupies.
	OutgoingCLTVSize = 4

	// NumPaddingBytes is the number of padding bytes in the hopData. These
	// bytes are currently unused within the protocol, and are reserved for
	// future use. However, if a hop contains extra data, then we'll
	// utilize this space to pack in the unrolled bytes.
	NumPaddingBytes = 12

	// LegacyHopDataSize is the fixed size of hop_data. BOLT 04 currently
	// specifies this to be 1 byte realm, 8 byte channel_id, 8 byte amount
	// to forward, 4 byte outgoing CLTV value, 12 bytes padding and 32 bytes
	// HMAC for a total of 65 bytes per hop.
	LegacyHopDataSize = (RealmByteSize + AddressSize + AmtForwardSize +
		OutgoingCLTVSize + NumPaddingBytes + HMACSize)

	// MaxPayloadSize is the maximum size a payload for a single hop can be.
	// This is the worst case scenario of a single hop, consuming all
	// available space. We need to know this in order to generate a
	// sufficiently long stream of pseudo-random bytes when
	// encrypting/decrypting the payload.
	MaxPayloadSize = routingInfoSize

	// routingInfoSize is the fixed size of the the routing info. This
	// consists of a addressSize byte address and a HMACSize byte HMAC for
	// each hop of the route, the first pair in cleartext and the following
	// pairs increasingly obfuscated. If not all space is used up, the
	// remainder is padded with null-bytes, also obfuscated.
	routingInfoSize = 1300

	// numStreamBytes is the number of bytes produced by our CSPRG for the
	// key stream implementing our stream cipher to encrypt/decrypt the mix
	// header. The MaxPayloadSize bytes at the end are used to
	// encrypt/decrypt the fillers when processing the packet of generating
	// the HMACs when creating the packet.
	numStreamBytes = routingInfoSize * 2

	// keyLen is the length of the keys used to generate cipher streams and
	// encrypt payloads. Since we use SHA256 to generate the keys, the
	// maximum length currently is 32 bytes.
	keyLen = 32

	// baseVersion represent the current supported version of onion packet.
	baseVersion = 0
)

var (
	ErrMaxRoutingInfoSizeExceeded = fmt.Errorf(
		"max routing info size of %v bytes exceeded", routingInfoSize)
)

// OnionPacket is the onion wrapped hop-to-hop routing information necessary to
// propagate a message through the mix-net without intermediate nodes having
// knowledge of their position within the route, the source, the destination,
// and finally the identities of the past/future nodes in the route. At each
// hop the ephemeral key is used by the node to perform ECDH between itself and
// the source node. This derived secret key is used to check the MAC of the
// entire mix header, decrypt the next set of routing information, and
// re-randomize the ephemeral key for the next node in the path. This per-hop
// re-randomization allows us to only propagate a single group element through
// the onion route.
type OnionPacket struct {
	// Version denotes the version of this onion packet. The version
	// indicates how a receiver of the packet should interpret the bytes
	// following this version byte. Currently, a version of 0x00 is the
	// only defined version type.
	Version byte

	// EphemeralKey is the public key that each hop will used in
	// combination with the private key in an ECDH to derive the shared
	// secret used to check the HMAC on the packet and also decrypted the
	// routing information.
	EphemeralKey *btcec.PublicKey

	// RoutingInfo is the full routing information for this onion packet.
	// This encodes all the forwarding instructions for this current hop
	// and all the hops in the route.
	RoutingInfo [routingInfoSize]byte

	// HeaderMAC is an HMAC computed with the shared secret of the routing
	// data and the associated data for this route. Including the
	// associated data lets each hop authenticate higher-level data that is
	// critical for the forwarding of this HTLC.
	HeaderMAC [HMACSize]byte
}

// generateSharedSecrets by the given nodes pubkeys, generates the shared
// secrets.
func generateSharedSecrets(paymentPath []*btcec.PublicKey,
	sessionKey *btcec.PrivateKey) []Hash256 {

	// Each hop performs ECDH with our ephemeral key pair to arrive at a
	// shared secret. Additionally, each hop randomizes the group element
	// for the next hop by multiplying it by the blinding factor. This way
	// we only need to transmit a single group element, and hops can't link
	// a session back to us if they have several nodes in the path.
	numHops := len(paymentPath)
	hopSharedSecrets := make([]Hash256, numHops)

	// Compute the triplet for the first hop outside of the main loop.
	// Within the loop each new triplet will be computed recursively based
	// off of the blinding factor of the last hop.
	lastEphemeralPubKey := sessionKey.PubKey()
	hopSharedSecrets[0] = generateSharedSecret(paymentPath[0], sessionKey)
	lastBlindingFactor := computeBlindingFactor(lastEphemeralPubKey, hopSharedSecrets[0][:])

	// The cached blinding factor will contain the running product of the
	// session private key x and blinding factors b_i, computed as
	//   c_0 = x
	//   c_i = c_{i-1} * b_{i-1} 		 (mod |F(G)|).
	//       = x * b_0 * b_1 * ... * b_{i-1} (mod |F(G)|).
	//
	// We begin with just the session private key x, so that base case
	// c_0 = x. At the beginning of each iteration, the previous blinding
	// factor is aggregated into the modular product, and used as the scalar
	// value in deriving the hop ephemeral keys and shared secrets.
	var cachedBlindingFactor big.Int
	cachedBlindingFactor.SetBytes(sessionKey.D.Bytes())

	// Now recursively compute the cached blinding factor, ephemeral ECDH
	// pub keys, and shared secret for each hop.
	var nextBlindingFactor big.Int
	for i := 1; i <= numHops-1; i++ {
		// Update the cached blinding factor with b_{i-1}.
		nextBlindingFactor.SetBytes(lastBlindingFactor[:])
		cachedBlindingFactor.Mul(&cachedBlindingFactor, &nextBlindingFactor)
		cachedBlindingFactor.Mod(&cachedBlindingFactor, btcec.S256().Params().N)

		// a_i = g ^ c_i
		//     = g^( x * b_0 * ... * b_{i-1} )
		//     = X^( b_0 * ... * b_{i-1} )
		// X_our_session_pub_key x all prev blinding factors
		lastEphemeralPubKey = blindBaseElement(cachedBlindingFactor.Bytes())

		// e_i = Y_i ^ c_i
		//     = ( Y_i ^ x )^( b_0 * ... * b_{i-1} )
		// (Y_their_pub_key x x_our_priv) x all prev blinding factors
		hopBlindedPubKey := blindGroupElement(
			paymentPath[i], cachedBlindingFactor.Bytes(),
		)

		// s_i = sha256( e_i )
		//     = sha256( Y_i ^ (x * b_0 * ... * b_{i-1} )
		hopSharedSecrets[i] = sha256.Sum256(hopBlindedPubKey.SerializeCompressed())

		// Only need to evaluate up to the penultimate blinding factor.
		if i >= numHops-1 {
			break
		}

		// b_i = sha256( a_i || s_i )
		lastBlindingFactor = computeBlindingFactor(
			lastEphemeralPubKey, hopSharedSecrets[i][:],
		)
	}

	return hopSharedSecrets
}

// NewOnionPacket creates a new onion packet which is capable of obliviously
// routing a message through the mix-net path outline by 'paymentPath'.
func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
	assocData []byte, pktFiller PacketFiller) (*OnionPacket, error) {

	// Check whether total payload size doesn't exceed the hard maximum.
	if paymentPath.TotalPayloadSize() > routingInfoSize {
		return nil, ErrMaxRoutingInfoSizeExceeded
	}

	// If we don't actually have a partially populated route, then we'll
	// exit early.
	numHops := paymentPath.TrueRouteLength()
	if numHops == 0 {
		return nil, fmt.Errorf("route of length zero passed in")
	}

	// We'll force the caller to provide a packet filler, as otherwise we
	// may default to an insecure filling method (which should only really
	// be used to generate test vectors).
	if pktFiller == nil {
		return nil, fmt.Errorf("packet filler must be specified")
	}

	hopSharedSecrets := generateSharedSecrets(
		paymentPath.NodeKeys(), sessionKey,
	)

	// Generate the padding, called "filler strings" in the paper.
	filler := generateHeaderPadding("rho", paymentPath, hopSharedSecrets)

	// Allocate zero'd out byte slices to store the final mix header packet
	// and the hmac for each hop.
	var (
		mixHeader     [routingInfoSize]byte
		nextHmac      [HMACSize]byte
		hopPayloadBuf bytes.Buffer
	)

	// Fill the packet using the caller specified methodology.
	if err := pktFiller(sessionKey, &mixHeader); err != nil {
		return nil, err
	}

	// Now we compute the routing information for each hop, along with a
	// MAC of the routing info using the shared key for that hop.
	for i := numHops - 1; i >= 0; i-- {
		// We'll derive the two keys we need for each hop in order to:
		// generate our stream cipher bytes for the mixHeader, and
		// calculate the MAC over the entire constructed packet.
		rhoKey := generateKey("rho", &hopSharedSecrets[i])
		muKey := generateKey("mu", &hopSharedSecrets[i])

		// The HMAC for the final hop is simply zeroes. This allows the
		// last hop to recognize that it is the destination for a
		// particular payment.
		paymentPath[i].HopPayload.HMAC = nextHmac

		// Next, using the key dedicated for our stream cipher, we'll
		// generate enough bytes to obfuscate this layer of the onion
		// packet.
		streamBytes := generateCipherStream(rhoKey, routingInfoSize)
		payload := paymentPath[i].HopPayload

		// Before we assemble the packet, we'll shift the current
		// mix-header to the right in order to make room for this next
		// per-hop data.
		shiftSize := payload.NumBytes()
		rightShift(mixHeader[:], shiftSize)

		err := payload.Encode(&hopPayloadBuf)
		if err != nil {
			return nil, err
		}

		copy(mixHeader[:], hopPayloadBuf.Bytes())

		// Once the packet for this hop has been assembled, we'll
		// re-encrypt the packet by XOR'ing with a stream of bytes
		// generated using our shared secret.
		xor(mixHeader[:], mixHeader[:], streamBytes[:])

		// If this is the "last" hop, then we'll override the tail of
		// the hop data.
		if i == numHops-1 {
			copy(mixHeader[len(mixHeader)-len(filler):], filler)
		}

		// The packet for this hop consists of: mixHeader. When
		// calculating the MAC, we'll also include the optional
		// associated data which can allow higher level applications to
		// prevent replay attacks.
		packet := append(mixHeader[:], assocData...)
		nextHmac = calcMac(muKey, packet)

		hopPayloadBuf.Reset()
	}

	return &OnionPacket{
		Version:      baseVersion,
		EphemeralKey: sessionKey.PubKey(),
		RoutingInfo:  mixHeader,
		HeaderMAC:    nextHmac,
	}, nil
}

// rightShift shifts the byte-slice by the given number of bytes to the right
// and 0-fill the resulting gap.
func rightShift(slice []byte, num int) {
	for i := len(slice) - num - 1; i >= 0; i-- {
		slice[num+i] = slice[i]
	}

	for i := 0; i < num; i++ {
		slice[i] = 0
	}
}

// generateHeaderPadding derives the bytes for padding the mix header to ensure
// it remains fixed sized throughout route transit. At each step, we add
// 'frameSize*frames' padding of zeroes, concatenate it to the previous filler,
// then decrypt it (XOR) with the secret key of the current hop. When
// encrypting the mix header we essentially do the reverse of this operation:
// we "encrypt" the padding, and drop 'frameSize*frames' number of zeroes. As
// nodes process the mix header they add the padding ('frameSize*frames') in
// order to check the MAC and decrypt the next routing information eventually
// leaving only the original "filler" bytes produced by this function at the
// last hop.  Using this methodology, the size of the field stays constant at
// each hop.
func generateHeaderPadding(key string, path *PaymentPath, sharedSecrets []Hash256) []byte {
	numHops := path.TrueRouteLength()

	// We have to generate a filler that matches all but the last hop (the
	// last hop won't generate an HMAC)
	fillerSize := path.TotalPayloadSize() - path[numHops-1].HopPayload.NumBytes()
	filler := make([]byte, fillerSize)

	for i := 0; i < numHops-1; i++ {
		// Sum up how many frames were used by prior hops.
		fillerStart := routingInfoSize
		for _, p := range path[:i] {
			fillerStart -= p.HopPayload.NumBytes()
		}

		// The filler is the part dangling off of the end of the
		// routingInfo, so offset it from there, and use the current
		// hop's frame count as its size.
		fillerEnd := routingInfoSize + path[i].HopPayload.NumBytes()

		streamKey := generateKey(key, &sharedSecrets[i])
		streamBytes := generateCipherStream(streamKey, numStreamBytes)

		xor(filler, filler, streamBytes[fillerStart:fillerEnd])
	}

	return filler
}

// Encode serializes the raw bytes of the onion packet into the passed
// io.Writer. The form encoded within the passed io.Writer is suitable for
// either storing on disk, or sending over the network.
func (f *OnionPacket) Encode(w io.Writer) error {
	ephemeral := f.EphemeralKey.SerializeCompressed()

	if _, err := w.Write([]byte{f.Version}); err != nil {
		return err
	}

	if _, err := w.Write(ephemeral); err != nil {
		return err
	}

	if _, err := w.Write(f.RoutingInfo[:]); err != nil {
		return err
	}

	if _, err := w.Write(f.HeaderMAC[:]); err != nil {
		return err
	}

	return nil
}

// Decode fully populates the target ForwardingMessage from the raw bytes
// encoded within the io.Reader. In the case of any decoding errors, an error
// will be returned. If the method success, then the new OnionPacket is ready
// to be processed by an instance of SphinxNode.
func (f *OnionPacket) Decode(r io.Reader) error {
	var err error

	var buf [1]byte
	if _, err := io.ReadFull(r, buf[:]); err != nil {
		return err
	}
	f.Version = buf[0]

	// If version of the onion packet protocol unknown for us than in might
	// lead to improperly decoded data.
	if f.Version != baseVersion {
		return ErrInvalidOnionVersion
	}

	var ephemeral [33]byte
	if _, err := io.ReadFull(r, ephemeral[:]); err != nil {
		return err
	}
	f.EphemeralKey, err = btcec.ParsePubKey(ephemeral[:], btcec.S256())
	if err != nil {
		return ErrInvalidOnionKey
	}

	if _, err := io.ReadFull(r, f.RoutingInfo[:]); err != nil {
		return err
	}

	if _, err := io.ReadFull(r, f.HeaderMAC[:]); err != nil {
		return err
	}

	return nil
}

// ProcessCode is an enum-like type which describes to the high-level package
// user which action should be taken after processing a Sphinx packet.
type ProcessCode int

const (
	// ExitNode indicates that the node which processed the Sphinx packet
	// is the destination hop in the route.
	ExitNode = iota

	// MoreHops indicates that there are additional hops left within the
	// route. Therefore the caller should forward the packet to the node
	// denoted as the "NextHop".
	MoreHops

	// Failure indicates that a failure occurred during packet processing.
	Failure
)

// String returns a human readable string for each of the ProcessCodes.
func (p ProcessCode) String() string {
	switch p {
	case ExitNode:
		return "ExitNode"
	case MoreHops:
		return "MoreHops"
	case Failure:
		return "Failure"
	default:
		return "Unknown"
	}
}

// ProcessedPacket encapsulates the resulting state generated after processing
// an OnionPacket. A processed packet communicates to the caller what action
// should be taken after processing.
type ProcessedPacket struct {
	// Action represents the action the caller should take after processing
	// the packet.
	Action ProcessCode

	// ForwardingInstructions is the per-hop payload recovered from the
	// initial encrypted onion packet. It details how the packet should be
	// forwarded and also includes information that allows the processor of
	// the packet to authenticate the information passed within the HTLC.
	//
	// NOTE: This field will only be populated iff the above Action is
	// MoreHops.
	ForwardingInstructions *HopData

	// Payload is the raw payload as extracted from the packet. If the
	// ForwardingInstructions field above is nil, then this is a modern TLV
	// payload. As a result, the caller should parse the contents to obtain
	// the new set of forwarding instructions.
	Payload HopPayload

	// NextPacket is the onion packet that should be forwarded to the next
	// hop as denoted by the ForwardingInstructions field.
	//
	// NOTE: This field will only be populated iff the above Action is
	// MoreHops.
	NextPacket *OnionPacket
}

// Router is an onion router within the Sphinx network. The router is capable
// of processing incoming Sphinx onion packets thereby "peeling" a layer off
// the onion encryption which the packet is wrapped with.
type Router struct {
	nodeID   [AddressSize]byte
	nodeAddr *btcutil.AddressPubKeyHash

	onionKey *btcec.PrivateKey

	log ReplayLog
}

// NewRouter creates a new instance of a Sphinx onion Router given the node's
// currently advertised onion private key, and the target Bitcoin network.
func NewRouter(nodeKey *btcec.PrivateKey, net *chaincfg.Params, log ReplayLog) *Router {
	var nodeID [AddressSize]byte
	copy(nodeID[:], btcutil.Hash160(nodeKey.PubKey().SerializeCompressed()))

	// Safe to ignore the error here, nodeID is 20 bytes.
	nodeAddr, _ := btcutil.NewAddressPubKeyHash(nodeID[:], net)

	return &Router{
		nodeID:   nodeID,
		nodeAddr: nodeAddr,
		onionKey: &btcec.PrivateKey{
			PublicKey: ecdsa.PublicKey{
				Curve: btcec.S256(),
				X:     nodeKey.X,
				Y:     nodeKey.Y,
			},
			D: nodeKey.D,
		},
		log: log,
	}
}

// Start starts / opens the ReplayLog's channeldb and its accompanying
// garbage collector goroutine.
func (r *Router) Start() error {
	return r.log.Start()
}

// Stop stops / closes the ReplayLog's channeldb and its accompanying
// garbage collector goroutine.
func (r *Router) Stop() {
	r.log.Stop()
}

// ProcessOnionPacket processes an incoming onion packet which has been forward
// to the target Sphinx router. If the encoded ephemeral key isn't on the
// target Elliptic Curve, then the packet is rejected. Similarly, if the
// derived shared secret has been seen before the packet is rejected.  Finally
// if the MAC doesn't check the packet is again rejected.
//
// In the case of a successful packet processing, and ProcessedPacket struct is
// returned which houses the newly parsed packet, along with instructions on
// what to do next.
func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket,
	assocData []byte, incomingCltv uint32) (*ProcessedPacket, error) {

	// Compute the shared secret for this onion packet.
	sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey)
	if err != nil {
		return nil, err
	}

	// Additionally, compute the hash prefix of the shared secret, which
	// will serve as an identifier for detecting replayed packets.
	hashPrefix := hashSharedSecret(&sharedSecret)

	// Continue to optimistically process this packet, deferring replay
	// protection until the end to reduce the penalty of multiple IO
	// operations.
	packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData, r)
	if err != nil {
		return nil, err
	}

	// Atomically compare this hash prefix with the contents of the on-disk
	// log, persisting it only if this entry was not detected as a replay.
	if err := r.log.Put(hashPrefix, incomingCltv); err != nil {
		return nil, err
	}

	return packet, nil
}

// ReconstructOnionPacket rederives the subsequent onion packet.
//
// NOTE: This method does not do any sort of replay protection, and should only
// be used to reconstruct packets that were successfully processed previously.
func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket,
	assocData []byte) (*ProcessedPacket, error) {

	// Compute the shared secret for this onion packet.
	sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey)
	if err != nil {
		return nil, err
	}

	return processOnionPacket(onionPkt, &sharedSecret, assocData, r)
}

// unwrapPacket wraps a layer of the passed onion packet using the specified
// shared secret and associated data. The associated data will be used to check
// the HMAC at each hop to ensure the same data is passed along with the onion
// packet. This function returns the next inner onion packet layer, along with
// the hop data extracted from the outer onion packet.
func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
	assocData []byte) (*OnionPacket, *HopPayload, error) {

	dhKey := onionPkt.EphemeralKey
	routeInfo := onionPkt.RoutingInfo
	headerMac := onionPkt.HeaderMAC

	// Using the derived shared secret, ensure the integrity of the routing
	// information by checking the attached MAC without leaking timing
	// information.
	message := append(routeInfo[:], assocData...)
	calculatedMac := calcMac(generateKey("mu", sharedSecret), message)
	if !hmac.Equal(headerMac[:], calculatedMac[:]) {
		return nil, nil, ErrInvalidOnionHMAC
	}

	// Attach the padding zeroes in order to properly strip an encryption
	// layer off the routing info revealing the routing information for the
	// next hop.
	streamBytes := generateCipherStream(
		generateKey("rho", sharedSecret), numStreamBytes,
	)
	zeroBytes := bytes.Repeat([]byte{0}, MaxPayloadSize)
	headerWithPadding := append(routeInfo[:], zeroBytes...)

	var hopInfo [numStreamBytes]byte
	xor(hopInfo[:], headerWithPadding, streamBytes)

	// Randomize the DH group element for the next hop using the
	// deterministic blinding factor.
	blindingFactor := computeBlindingFactor(dhKey, sharedSecret[:])
	nextDHKey := blindGroupElement(dhKey, blindingFactor[:])

	// With the MAC checked, and the payload decrypted, we can now parse
	// out the payload so we can derive the specified forwarding
	// instructions.
	var hopPayload HopPayload
	if err := hopPayload.Decode(bytes.NewReader(hopInfo[:])); err != nil {
		return nil, nil, err
	}

	// With the necessary items extracted, we'll copy of the onion packet
	// for the next node, snipping off our per-hop data.
	var nextMixHeader [routingInfoSize]byte
	copy(nextMixHeader[:], hopInfo[hopPayload.NumBytes():])
	innerPkt := &OnionPacket{
		Version:      onionPkt.Version,
		EphemeralKey: nextDHKey,
		RoutingInfo:  nextMixHeader,
		HeaderMAC:    hopPayload.HMAC,
	}

	return innerPkt, &hopPayload, nil
}

// processOnionPacket performs the primary key derivation and handling of onion
// packets. The processed packets returned from this method should only be used
// if the packet was not flagged as a replayed packet.
func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
	assocData []byte,
	sharedSecretGen sharedSecretGenerator) (*ProcessedPacket, error) {

	// First, we'll unwrap an initial layer of the onion packet. Typically,
	// we'll only have a single layer to unwrap, However, if the sender has
	// additional data for us within the Extra Onion Blobs (EOBs), then we
	// may have to unwrap additional layers.  By default, the inner most
	// mix header is the one that we'll want to pass onto the next hop so
	// they can properly check the HMAC and unwrap a layer for their
	// handoff hop.
	innerPkt, outerHopPayload, err := unwrapPacket(
		onionPkt, sharedSecret, assocData,
	)
	if err != nil {
		return nil, err
	}

	// By default we'll assume that there are additional hops in the route.
	// However if the uncovered 'nextMac' is all zeroes, then this
	// indicates that we're the final hop in the route.
	var action ProcessCode = MoreHops
	if bytes.Compare(zeroHMAC[:], outerHopPayload.HMAC[:]) == 0 {
		action = ExitNode
	}

	hopData, err := outerHopPayload.HopData()
	if err != nil {
		return nil, err
	}

	// Finally, we'll return a fully processed packet with the outer most
	// hop data (where the primary forwarding instructions lie) and the
	// inner most onion packet that we unwrapped.
	return &ProcessedPacket{
		Action:                 action,
		ForwardingInstructions: hopData,
		Payload:                *outerHopPayload,
		NextPacket:             innerPkt,
	}, nil
}

// Tx is a transaction consisting of a number of sphinx packets to be atomically
// written to the replay log. This structure helps to coordinate construction of
// the underlying Batch object, and to ensure that the result of the processing
// is idempotent.
type Tx struct {
	// batch is the set of packets to be incrementally processed and
	// ultimately committed in this transaction
	batch *Batch

	// router is a reference to the sphinx router that created this
	// transaction. Committing this transaction will utilize this router's
	// replay log.
	router *Router

	// packets contains a potentially sparse list of optimistically processed
	// packets for this batch. The contents of a particular index should
	// only be accessed if the index is *not* included in the replay set, or
	// otherwise failed any other stage of the processing.
	packets []ProcessedPacket
}

// BeginTxn creates a new transaction that can later be committed back to the
// sphinx router's replay log.
//
// NOTE: The nels parameter should represent the maximum number of that could
// be added to the batch, using sequence numbers that match or exceed this
// value could result in an out-of-bounds panic.
func (r *Router) BeginTxn(id []byte, nels int) *Tx {
	return &Tx{
		batch:   NewBatch(id),
		router:  r,
		packets: make([]ProcessedPacket, nels),
	}
}

// ProcessOnionPacket processes an incoming onion packet which has been forward
// to the target Sphinx router. If the encoded ephemeral key isn't on the
// target Elliptic Curve, then the packet is rejected. Similarly, if the
// derived shared secret has been seen before the packet is rejected.  Finally
// if the MAC doesn't check the packet is again rejected.
//
// In the case of a successful packet processing, and ProcessedPacket struct is
// returned which houses the newly parsed packet, along with instructions on
// what to do next.
func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket,
	assocData []byte, incomingCltv uint32) error {

	// Compute the shared secret for this onion packet.
	sharedSecret, err := t.router.generateSharedSecret(
		onionPkt.EphemeralKey,
	)
	if err != nil {
		return err
	}

	// Additionally, compute the hash prefix of the shared secret, which
	// will serve as an identifier for detecting replayed packets.
	hashPrefix := hashSharedSecret(&sharedSecret)

	// Continue to optimistically process this packet, deferring replay
	// protection until the end to reduce the penalty of multiple IO
	// operations.
	packet, err := processOnionPacket(
		onionPkt, &sharedSecret, assocData, t.router,
	)
	if err != nil {
		return err
	}

	// Add the hash prefix to pending batch of shared secrets that will be
	// written later via Commit().
	err = t.batch.Put(seqNum, hashPrefix, incomingCltv)
	if err != nil {
		return err
	}

	// If we successfully added this packet to the batch, cache the
	// processed packet within the Tx which can be accessed after
	// committing if this sequence number does not appear in the replay
	// set.
	t.packets[seqNum] = *packet

	return nil
}

// Commit writes this transaction's batch of sphinx packets to the replay log,
// performing a final check against the log for replays.
func (t *Tx) Commit() ([]ProcessedPacket, *ReplaySet, error) {
	if t.batch.IsCommitted {
		return t.packets, t.batch.ReplaySet, nil
	}

	rs, err := t.router.log.PutBatch(t.batch)

	return t.packets, rs, err
}
