diff --git a/attr_error_structure.go b/attr_error_structure.go
new file mode 100644
index 0000000..05b52f2
--- /dev/null
+++ b/attr_error_structure.go
@@ -0,0 +1,153 @@
+package sphinx
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"io"
+)
+
+// AttrErrorStructure contains the parameters that define the structure
+// of the error message that is passed back.
+type AttrErrorStructure struct {
+	// hopCount is the assumed maximum number of hops in the path.
+	hopCount int
+
+	// fixedPayloadLen is the length of the payload data that each hop along
+	// the route can add.
+	fixedPayloadLen int
+
+	// hmacSize is the number of bytes that is reserved for each hmac.
+	hmacSize int
+
+	zeroHmac []byte
+}
+
+// NewAttrErrorStructure creates an AttrErrorStructure with the defined
+// parameters and returns it.
+func NewAttrErrorStructure(hopCount int, fixedPayloadLen int,
+	hmacSize int) *AttrErrorStructure {
+
+	return &AttrErrorStructure{
+		hopCount:        hopCount,
+		fixedPayloadLen: fixedPayloadLen,
+		hmacSize:        hmacSize,
+
+		zeroHmac: make([]byte, hmacSize),
+	}
+}
+
+// HopCount returns the assumed maximum number of hops in the path.
+func (o *AttrErrorStructure) HopCount() int {
+	return o.hopCount
+}
+
+// FixedPayloadLen returns the length of the payload data that each hop along
+// the route can add.
+func (o *AttrErrorStructure) FixedPayloadLen() int {
+	return o.fixedPayloadLen
+}
+
+// HmacSize returns the number of bytes that is reserved for each hmac.
+func (o *AttrErrorStructure) HmacSize() int {
+	return o.hmacSize
+}
+
+// totalHmacs is the total number of hmacs that is present in the failure
+// message. Every hop adds HopCount hmacs to the message, but as the error
+// back-propagates, downstream hmacs can be pruned. This results in the number
+// of hmacs for each hop decreasing by one for each step that we move away from
+// the current node.
+func (o *AttrErrorStructure) totalHmacs() int {
+	return (o.hopCount * (o.hopCount + 1)) / 2
+}
+
+// allHmacsLen is the total length in the bytes of all hmacs in the failure
+// message.
+func (o *AttrErrorStructure) allHmacsLen() int {
+	return o.totalHmacs() * o.hmacSize
+}
+
+// hmacsAndPayloadsLen is the total length in bytes of all hmacs and payloads
+// together.
+func (o *AttrErrorStructure) hmacsAndPayloadsLen() int {
+	return o.allHmacsLen() + o.allPayloadsLen()
+}
+
+// allPayloadsLen is the total length in bytes of all payloads in the failure
+// message.
+func (o *AttrErrorStructure) allPayloadsLen() int {
+	return o.payloadLen() * o.hopCount
+}
+
+// payloadLen is the size of the per-node payload. It consists of a 1-byte
+// payload type followed by the payload data.
+func (o *AttrErrorStructure) payloadLen() int {
+	return o.fixedPayloadLen
+}
+
+// payloads returns a slice containing all payloads in the given failure
+// data block. The payloads follow the message in the block.
+func (o *AttrErrorStructure) payloads(data []byte) []byte {
+	dataLen := len(data)
+
+	return data[dataLen-o.hmacsAndPayloadsLen() : dataLen-o.allHmacsLen()]
+}
+
+// hmacs returns a slice containing all hmacs in the given failure data block.
+// The hmacs are positioned at the end of the data block.
+func (o *AttrErrorStructure) hmacs(data []byte) []byte {
+	return data[len(data)-o.allHmacsLen():]
+}
+
+// calculateHmac calculates an hmac given a shared secret and a presumed
+// position in the path. Position is expressed as the distance to the error
+// source. The error source itself is at position 0.
+func (o *AttrErrorStructure) calculateHmac(sharedSecret Hash256,
+	position int, message, payloads, hmacs []byte) []byte {
+
+	umKey := generateKey("um", &sharedSecret)
+	hash := hmac.New(sha256.New, umKey[:])
+
+	// Include message.
+	_, _ = hash.Write(message)
+
+	// Include payloads including our own.
+	_, _ = hash.Write(payloads[:(position+1)*o.payloadLen()])
+
+	// Include downstream hmacs.
+	writeDownstreamHmacs(position, o.hopCount, hmacs, o.hmacSize, hash)
+
+	hmac := hash.Sum(nil)
+
+	return hmac[:o.hmacSize]
+}
+
+// writeDownstreamHmacs writes the hmacs of downstream nodes that are relevant
+// for the given position to a writer instance. Position is expressed as the
+// distance to the error source. The error source itself is at position 0.
+func writeDownstreamHmacs(position, maxHops int, hmacs []byte, hmacBytes int,
+	w io.Writer) {
+
+	// Track the index of the next hmac to write in a variable. The first
+	// maxHops slots are reserved for the hmacs of the current hop and can
+	// therefore be skipped. The first hmac to write is part of the block of
+	// hmacs that was written by the first downstream node. Which hmac
+	// exactly is determined by the assumed position of the current node.
+	var hmacIdx = maxHops + (maxHops - position - 1)
+
+	// Iterate over all downstream nodes.
+	for j := 0; j < position; j++ {
+		_, _ = w.Write(
+			hmacs[hmacIdx*hmacBytes : (hmacIdx+1)*hmacBytes],
+		)
+
+		// Calculate the total number of hmacs in the block of the
+		// current downstream node.
+		blockSize := maxHops - j - 1
+
+		// Skip to the next block. The new hmac index will point to the
+		// hmac that corresponds to the next downstream node which is
+		// one step closer to the assumed error source.
+		hmacIdx += blockSize
+	}
+}
diff --git a/attr_error_test.go b/attr_error_test.go
new file mode 100644
index 0000000..e48028a
--- /dev/null
+++ b/attr_error_test.go
@@ -0,0 +1,454 @@
+package sphinx
+
+import (
+	"bytes"
+	"encoding/hex"
+	"encoding/json"
+	"os"
+	"testing"
+
+	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/stretchr/testify/require"
+)
+
+var attributableErrorTestStructure = NewAttrErrorStructure(20, 4, 4)
+
+// TestAttributableOnionFailure checks the ability of sender of payment to
+// decode the obfuscated onion error.
+func TestAttributableOnionFailure(t *testing.T) {
+	// Create numHops random sphinx paymentPath.
+	sessionKey, paymentPath := generateRandomPath(t)
+
+	// Reduce the error path on one node, in order to check that we are
+	// able to receive the error not only from last hop.
+	errorPath := paymentPath[:len(paymentPath)-1]
+
+	failureData := bytes.Repeat([]byte{'A'}, minOnionErrorLength)
+	sharedSecrets, _, err := generateSharedSecrets(paymentPath, sessionKey)
+	require.NoError(t, err)
+
+	// Emulate creation of the obfuscator on node where error have occurred.
+	obfuscator := NewOnionErrorEncrypter(
+		sharedSecrets[len(errorPath)-1], attributableErrorTestStructure,
+	)
+
+	// Emulate the situation when last hop creates the onion failure
+	// message and send it back.
+	holdTime := uint32(42)
+	var attrData, legacyData []byte
+
+	legacyData, attrData, err = obfuscator.EncryptError(
+		true, failureData, attrData, holdTime,
+	)
+	require.NoError(t, err)
+	payloads := []uint32{holdTime}
+
+	// Emulate that failure message is backward obfuscated on every hop.
+	for i := len(errorPath) - 2; i >= 0; i-- {
+		// Emulate creation of the obfuscator on forwarding node which
+		// propagates the onion failure.
+		obfuscator = NewOnionErrorEncrypter(
+			sharedSecrets[i], attributableErrorTestStructure,
+		)
+
+		holdTimeIntermediate := uint32(100 + i)
+		legacyData, attrData, err = obfuscator.EncryptError(
+			false, legacyData, attrData, holdTimeIntermediate,
+		)
+		require.NoError(t, err)
+
+		payloads = append([]uint32{holdTimeIntermediate}, payloads...)
+	}
+
+	// Emulate creation of the deobfuscator on the receiving onion error
+	// side.
+	deobfuscator := NewOnionErrorDecrypter(&Circuit{
+		SessionKey:  sessionKey,
+		PaymentPath: paymentPath,
+	}, attributableErrorTestStructure)
+
+	// Emulate that sender node receive the failure message and trying to
+	// unwrap it, by applying obfuscation and checking the hmac.
+	decryptedError, err := deobfuscator.DecryptError(
+		legacyData, attrData, true,
+	)
+	require.NoError(t, err)
+
+	// We should understand the node from which error have been received.
+	require.Equal(t,
+		errorPath[len(errorPath)-1].SerializeCompressed(),
+		decryptedError.Sender.SerializeCompressed())
+
+	require.Equal(t, len(errorPath), decryptedError.SenderIdx)
+
+	// Check that message have been properly de-obfuscated.
+	require.Equal(t, failureData, decryptedError.Message)
+	require.Equal(t, payloads, decryptedError.HoldTimes)
+}
+
+// TestOnionFailureCorruption checks the ability of sender of payment to
+// identify a node on the path that corrupted the failure message.
+func TestOnionFailureCorruption(t *testing.T) {
+	t.Parallel()
+
+	// Create numHops random sphinx paymentPath.
+	sessionKey, paymentPath := generateRandomPath(t)
+
+	// Reduce the error path on one node, in order to check that we are
+	// able to receive the error not only from last hop.
+	errorPath := paymentPath[:len(paymentPath)-1]
+
+	failureData := bytes.Repeat([]byte{'A'}, minOnionErrorLength)
+	sharedSecrets, _, err := generateSharedSecrets(paymentPath, sessionKey)
+	require.NoError(t, err)
+
+	// Emulate creation of the obfuscator on node where error have occurred.
+	obfuscator := NewOnionErrorEncrypter(
+		sharedSecrets[len(errorPath)-1], attributableErrorTestStructure,
+	)
+
+	// Emulate the situation when last hop creates the onion failure
+	// message and send it back.
+	holdTime := uint32(1)
+	var attrData []byte
+	legacyData, attrData, err := obfuscator.EncryptError(
+		true, failureData, attrData, holdTime,
+	)
+	require.NoError(t, err)
+
+	// Emulate that failure message is backward obfuscated on every hop.
+	for i := len(errorPath) - 2; i >= 0; i-- {
+		// Emulate creation of the obfuscator on forwarding node which
+		// propagates the onion failure.
+		obfuscator = NewOnionErrorEncrypter(
+			sharedSecrets[i], attributableErrorTestStructure,
+		)
+
+		holdTime := uint32(100 + i)
+		legacyData, attrData, err = obfuscator.EncryptError(
+			false, failureData, attrData, holdTime,
+		)
+		require.NoError(t, err)
+
+		// Hop 1 (the second hop from the sender pov) is corrupting the
+		// failure message.
+		if i == 1 {
+			attrData[0] ^= 255
+		}
+	}
+
+	// Emulate creation of the deobfuscator on the receiving onion error
+	// side.
+	deobfuscator := NewOnionErrorDecrypter(&Circuit{
+		SessionKey:  sessionKey,
+		PaymentPath: paymentPath,
+	}, attributableErrorTestStructure)
+
+	// Emulate that sender node receive the failure message and trying to
+	// unwrap it, by applying obfuscation and checking the hmac.
+	decryptedError, err := deobfuscator.DecryptError(
+		legacyData, attrData, true,
+	)
+	require.NoError(t, err)
+
+	// Assert that the second hop is correctly identified as the error
+	// source.
+	require.Equal(t, 2, decryptedError.SenderIdx)
+	require.Nil(t, decryptedError.Message)
+}
+
+type specHop struct {
+	SharedSecret     string `json:"sharedSecret"`
+	EncryptedMessage string `json:"encryptedMessage"`
+}
+
+type generatedData struct {
+	EncodedFailureMessage string `json:"encodedFailureMessage"`
+
+	Hops []specHop `json:"hops"`
+}
+
+type specVector struct {
+	generatedData `json:"generate"`
+}
+
+// TestOnionFailureSpecVector checks that onion error corresponds to the
+// specification.
+func TestAttributableFailureSpecVector(t *testing.T) {
+	t.Parallel()
+
+	vectorBytes, err := os.ReadFile("testdata/onion-test.json")
+	require.NoError(t, err)
+
+	var vector specVector
+	require.NoError(t, json.Unmarshal(vectorBytes, &vector))
+
+	failureData, err := hex.DecodeString(
+		vector.generatedData.EncodedFailureMessage,
+	)
+	require.NoError(t, err)
+
+	paymentPath, err := getSpecPubKeys()
+	require.NoError(t, err)
+
+	sessionKey, err := getSpecSessionKey()
+	require.NoError(t, err)
+
+	var (
+		legacyData []byte
+		attrData   []byte
+	)
+	sharedSecrets, _, err := generateSharedSecrets(paymentPath, sessionKey)
+	require.NoError(t, err)
+
+	for i, test := range vector.Hops {
+		// Decode the shared secret and check that it matchs with
+		// specification.
+		expectedSharedSecret, err := hex.DecodeString(test.SharedSecret)
+		require.NoError(t, err)
+
+		obfuscator := NewOnionErrorEncrypter(
+			sharedSecrets[len(sharedSecrets)-1-i],
+			attributableErrorTestStructure,
+		)
+
+		require.Equal(
+			t, expectedSharedSecret, obfuscator.sharedSecret[:],
+		)
+
+		holdTime := uint32(i + 1)
+
+		if i == 0 {
+			// Emulate the situation when last hop creates the onion
+			// failure message and send it back.
+			legacyData, attrData, err = obfuscator.EncryptError(
+				true, failureData, attrData, holdTime,
+			)
+			require.NoError(t, err)
+		} else {
+			// Emulate the situation when forward node obfuscates
+			// the onion failure.
+			legacyData, attrData, err = obfuscator.EncryptError(
+				false, legacyData, attrData, holdTime,
+			)
+			require.NoError(t, err)
+		}
+
+		// Decode the obfuscated data and check that it matches the
+		// specification.
+		expectedEncryptErrorData, err := hex.DecodeString(
+			test.EncryptedMessage,
+		)
+		require.NoError(t, err)
+
+		require.Equal(t, expectedEncryptErrorData, attrData)
+	}
+
+	deobfuscator := NewOnionErrorDecrypter(&Circuit{
+		SessionKey:  sessionKey,
+		PaymentPath: paymentPath,
+	}, attributableErrorTestStructure)
+
+	// Emulate that sender node receives the failure message and trying to
+	// unwrap it, by applying obfuscation and checking the hmac.
+	decryptedError, err := deobfuscator.DecryptError(
+		legacyData, attrData, true,
+	)
+	require.NoError(t, err)
+
+	// Check that message have been properly de-obfuscated.
+	require.Equal(t, failureData, decryptedError.Message)
+
+	// We should understand the node from which error have been received.
+	require.Equal(t,
+		paymentPath[len(paymentPath)-1].SerializeCompressed(),
+		decryptedError.Sender.SerializeCompressed(),
+	)
+
+	require.Equal(t, len(paymentPath), decryptedError.SenderIdx)
+
+	// Now let's verify the attributable error fields.
+	require.Equal(t, decryptedError.Message, failureData)
+
+	require.Equal(t,
+		paymentPath[len(paymentPath)-1].SerializeCompressed(),
+		decryptedError.Sender.SerializeCompressed(),
+	)
+
+	require.Equal(t, len(paymentPath), decryptedError.SenderIdx)
+}
+
+// TestAttributableOnionFailureZeroesMessage checks that a garbage failure is
+// attributed to the first hop.
+func TestAttributableOnionFailureZeroesMessage(t *testing.T) {
+	t.Parallel()
+
+	// Create numHops random sphinx paymentPath.
+	sessionKey, paymentPath := generateRandomPath(t)
+
+	// Emulate creation of the deobfuscator on the receiving onion error
+	// side.
+	deobfuscator := NewOnionErrorDecrypter(&Circuit{
+		SessionKey:  sessionKey,
+		PaymentPath: paymentPath,
+	}, attributableErrorTestStructure)
+
+	// Emulate that sender node receive the failure message and trying to
+	// unwrap it, by applying obfuscation and checking the hmac.
+	obfuscatedData := make([]byte, 20000)
+
+	decryptedError, err := deobfuscator.DecryptError(
+		obfuscatedData, nil, true,
+	)
+	require.NoError(t, err)
+
+	require.Equal(t, 1, decryptedError.SenderIdx)
+}
+
+// TestAttributableOnionFailureShortMessage checks that too short failure is
+// attributed to the first hop.
+func TestAttributableOnionFailureShortMessage(t *testing.T) {
+	t.Parallel()
+
+	// Create numHops random sphinx paymentPath.
+	sessionKey, paymentPath := generateRandomPath(t)
+
+	// Emulate creation of the deobfuscator on the receiving onion error
+	// side.
+	deobfuscator := NewOnionErrorDecrypter(&Circuit{
+		SessionKey:  sessionKey,
+		PaymentPath: paymentPath,
+	}, attributableErrorTestStructure)
+
+	// Emulate that sender node receive the failure message and trying to
+	// unwrap it, by applying obfuscation and checking the hmac.
+	obfuscatedData := make([]byte, deobfuscator.hmacsAndPayloadsLen()-1)
+	failureMsg := bytes.Repeat([]byte{1}, minOnionErrorLength)
+
+	decryptedError, err := deobfuscator.DecryptError(
+		failureMsg, obfuscatedData, true,
+	)
+	require.NoError(t, err)
+
+	require.Equal(t, 1, decryptedError.SenderIdx)
+	require.Equal(t, 1, decryptedError.SenderIdx)
+}
+
+func generateRandomPath(t *testing.T) (*btcec.PrivateKey, []*btcec.PublicKey) {
+	paymentPath := make([]*btcec.PublicKey, 5)
+	for i := 0; i < len(paymentPath); i++ {
+		privKey, err := btcec.NewPrivateKey()
+		require.NoError(t, err)
+
+		paymentPath[i] = privKey.PubKey()
+	}
+
+	sessionKey, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{'A'}, 32))
+
+	return sessionKey, paymentPath
+}
+
+func generateHashList(values ...int) []byte {
+	var b bytes.Buffer
+	for _, v := range values {
+		hash := [32]byte{byte(v)}
+		b.Write(hash[:])
+	}
+
+	return b.Bytes()
+}
+
+const testMaxHops = 4
+
+// Generate a list of 4+3+2+1 = 10 unique hmacs. The length of this list is
+// fixed for the chosen maxHops.
+func createTestHmacs() []byte {
+	return generateHashList(
+		43, 42, 41, 40,
+		32, 31, 30,
+		21, 20,
+		10,
+	)
+}
+
+const testHmacBytes = 32
+
+func TestWriteDownstreamHmacs(t *testing.T) {
+	require := require.New(t)
+
+	hmacs := createTestHmacs()
+
+	test := func(position int, expectedValues []int) {
+		var b bytes.Buffer
+		writeDownstreamHmacs(
+			position, testMaxHops, hmacs, testHmacBytes, &b,
+		)
+
+		expectedHashes := generateHashList(expectedValues...)
+		require.Equal(expectedHashes, b.Bytes())
+	}
+
+	// Assuming the current node is in the position furthest away from the
+	// error source, we expect three downstream hmacs to be relevant.
+	test(3, []int{32, 21, 10})
+
+	// Assuming the current node is in positions closer to the error source,
+	// fewer hmacs become relevant.
+	test(2, []int{31, 20})
+	test(1, []int{30})
+	test(0, []int{})
+}
+
+func TestShiftHmacsRight(t *testing.T) {
+	require := require.New(t)
+
+	hmacs := createTestHmacs()
+
+	o := NewOnionErrorEncrypter(
+		Hash256{},
+		NewAttrErrorStructure(testMaxHops, 0, 32),
+	)
+	o.shiftHmacsRight(hmacs)
+
+	expectedHmacs := generateHashList(
+		// Previous values are zeroed out.
+		0, 0, 0, 0,
+
+		// Previous first node hmacs minus the hmac representing the
+		// position farthest away from the error source.
+		42, 41, 40,
+
+		// And so on for the other nodes.
+		31, 30,
+		20,
+	)
+
+	require.Equal(expectedHmacs, hmacs)
+}
+
+func TestShiftHmacsLeft(t *testing.T) {
+	require := require.New(t)
+
+	hmacs := createTestHmacs()
+
+	o := NewOnionErrorDecrypter(
+		nil,
+		NewAttrErrorStructure(testMaxHops, 0, 32),
+	)
+	o.shiftHmacsLeft(hmacs)
+
+	expectedHmacs := generateHashList(
+		// The hmacs of the second hop now become the first hop hmacs.
+		// The slot corresponding to the position farthest away from the
+		// error source remains empty. Because we are shifting, this can
+		// never be the position of the first hop.
+		0, 32, 31, 30,
+
+		// Continue this same scheme for the downstream hops.
+		0, 21, 20,
+		0, 10,
+		0,
+	)
+
+	require.Equal(expectedHmacs, hmacs)
+}
diff --git a/crypto.go b/crypto.go
index 6b1b49e..d1845bb 100644
--- a/crypto.go
+++ b/crypto.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"crypto/hmac"
 	"crypto/sha256"
+	"encoding/binary"
 	"errors"
 	"fmt"
 
@@ -18,6 +19,14 @@ const (
 	// the onion. Any value lower than 32 will truncate the HMAC both
 	// during onion creation as well as during the verification.
 	HMACSize = 32
+
+	// AMMAG is the string representation for the ammag key type. Used in
+	// cypher stream generation.
+	AMMAG = "ammag"
+
+	// AMMAG_EXT is the string representation for the extended ammag key
+	// type. user in cypher stream generation.
+	AMMAG_EXT = "ammagext"
 )
 
 // chaChaPolyZeroNonce is a slice of zero bytes used in the chacha20poly1305
@@ -97,6 +106,10 @@ type DecryptedError struct {
 
 	// Message is the decrypted error message.
 	Message []byte
+
+	// HoldTimes is an array of hold times reported by each node on the error
+	// path.
+	HoldTimes []uint32
 }
 
 // zeroHMAC is the special HMAC value that allows the final node to determine
@@ -234,18 +247,18 @@ func chacha20polyDecrypt(key, cipherTxt []byte) ([]byte, error) {
 //
 // TODO(roasbef): rename?
 type sharedSecretGenerator interface {
-	// generateSharedSecret given a public key, generates a shared secret
+	// GenerateSharedSecret given a public key, generates a shared secret
 	// using private data of the underlying sharedSecretGenerator.
-	generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error)
+	GenerateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error)
 }
 
-// generateSharedSecret generates the shared secret using the given ephemeral
+// GenerateSharedSecret generates the shared secret using the given ephemeral
 // pub key and the Router's private key. If a blindingPoint is provided then it
 // is used to tweak the Router's private key before creating the shared secret
 // with the ephemeral pub key. The blinding point is used to determine our
 // shared secret with the receiver. From that we can determine our shared
 // secret with the sender using the dhKey.
-func (r *Router) generateSharedSecret(dhKey,
+func (r *Router) GenerateSharedSecret(dhKey,
 	blindingPoint *btcec.PublicKey) (Hash256, error) {
 
 	// If no blinding point is provided, then the un-tweaked dhKey can
@@ -301,37 +314,68 @@ func sharedSecret(priv SingleKeyECDH, pub *btcec.PublicKey) (Hash256, error) {
 // onionEncrypt obfuscates the data with compliance with BOLT#4. As we use a
 // stream cipher, calling onionEncrypt on an already encrypted piece of data
 // will decrypt it.
-func onionEncrypt(sharedSecret *Hash256, data []byte) []byte {
+func onionEncrypt(keyType string, sharedSecret *Hash256, data []byte) []byte {
 	p := make([]byte, len(data))
 
-	ammagKey := generateKey("ammag", sharedSecret)
+	ammagKey := generateKey(keyType, sharedSecret)
 	streamBytes := generateCipherStream(ammagKey, uint(len(data)))
 	xor(p, data, streamBytes)
 
 	return p
 }
 
-// minOnionErrorLength is the minimally expected length of the onion error
-// message. Including padding, all messages on the wire should be at least 256
-// bytes. We then add the size of the sha256 HMAC as well.
-const minOnionErrorLength = 2 + 2 + 256 + sha256.Size
+// minPaddedOnionErrorLength is the minimally expected length of the padded
+// onion error message including two uint16s for the length of the message and
+// the length of the padding.
+const minPaddedOnionErrorLength = 2 + 2 + 256
+
+// minOnionErrorLength is the minimally expected length of the complete onion
+// error message including the HMAC.
+const minOnionErrorLength = minPaddedOnionErrorLength + sha256.Size
 
 // DecryptError attempts to decrypt the passed encrypted error response. The
 // onion failure is encrypted in backward manner, starting from the node where
 // error have occurred. As a result, in order to decrypt the error we need get
 // all shared secret and apply decryption in the reverse order. A structure is
-// returned that contains the decrypted error message and information on the
-// sender.
-func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
-	*DecryptedError, error) {
+// returned that contains the decrypted error message and information of the
+// error sender. We also report the hold times in ms for each hop on the error
+// path.
+//
+// The strictAttribution flag controls the behavior of the decryption logic
+// surrounding the presence of attribution data:
+//
+//   - If set, then the first node with bad attribution data will be blamed
+//     immediately.
+//
+//   - If unset, decryption continues optimistically until a successful error
+//     message decryption occurs, regardless of attribution data validity. Hold
+//     times are still extracted for nodes that provided valid attribution data.
+func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte,
+	attrData []byte, strictAttribution bool) (*DecryptedError, error) {
 
-	// Ensure the error message length is as expected.
+	// Ensure the error message field is present and has the correct length.
 	if len(encryptedData) < minOnionErrorLength {
 		return nil, fmt.Errorf("invalid error length: "+
 			"expected at least %v got %v", minOnionErrorLength,
 			len(encryptedData))
 	}
 
+	validAttr := true
+
+	// If we're decrypting with strict attribution, we need to have the
+	// correct attribution data present too. If strictAttribution is set
+	// then we immediately blame the first hop.
+	if len(attrData) < o.hmacsAndPayloadsLen() {
+		if strictAttribution {
+			return &DecryptedError{
+				Sender:    o.circuit.PaymentPath[0],
+				SenderIdx: 1,
+			}, nil
+		} else {
+			validAttr = false
+		}
+	}
+
 	sharedSecrets, _, err := generateSharedSecrets(
 		o.circuit.PaymentPath,
 		o.circuit.SessionKey,
@@ -348,10 +392,16 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
 	)
 	copy(dummySecret[:], bytes.Repeat([]byte{1}, 32))
 
+	// Copy the failure message data in a new variable.
+	failData := make([]byte, len(encryptedData))
+	copy(failData, encryptedData)
+
+	hopPayloads := make([]uint32, 0)
+
 	// We'll iterate a constant amount of hops to ensure that we don't give
 	// away an timing information pertaining to the position in the route
 	// that the error emanated from.
-	for i := 0; i < NumMaxHops; i++ {
+	for i := 0; i < o.hopCount; i++ {
 		var sharedSecret Hash256
 
 		// If we've already found the sender, then we'll use our dummy
@@ -365,13 +415,74 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
 		}
 
 		// With the shared secret, we'll now strip off a layer of
-		// encryption from the encrypted error payload.
-		encryptedData = onionEncrypt(&sharedSecret, encryptedData)
+		// encryption from the encrypted failure and attribution
+		// data. This needs to be done before parsing the attribution
+		// data, as the attribution data HMACs commit to it.
+		failData = onionEncrypt(AMMAG, &sharedSecret, failData)
+
+		// If the attribution data are valid then do another round of
+		// attribution data decryption.
+		if validAttr {
+			attrData = onionEncrypt(
+				AMMAG_EXT, &sharedSecret, attrData,
+			)
+
+			payloads := o.payloads(attrData)
+			hmacs := o.hmacs(attrData)
+
+			// Let's calculate the HMAC we expect for the
+			// corresponding payloads.
+			position := o.hopCount - i - 1
+			expectedAttrHmac := o.calculateHmac(
+				sharedSecret, position, failData, payloads,
+				hmacs,
+			)
+
+			// Let's retrieve the actual HMAC from the correct
+			// position in the HMACs array.
+			actualAttrHmac := hmacs[i*o.hmacSize : (i+1)*o.hmacSize]
+
+			// If the hmac does not match up, exit with a nil
+			// message. This is not done for the dummy iterations.
+			if !bytes.Equal(actualAttrHmac, expectedAttrHmac) &&
+				sender == 0 && i < len(o.circuit.PaymentPath) {
+
+				switch strictAttribution {
+				case true:
+					sender = i + 1
+					msg = nil
+
+				case false:
+					// Flag the attribution data as invalid
+					// from this point onwards. This will
+					// prevent the loop from trying to
+					// extract anything from the attribution
+					// data.
+					validAttr = false
+				}
+			}
+
+			// Extract the payload and exit with a nil message if it
+			// is invalid.
+			holdTime := o.extractPayload(payloads)
+			if sender == 0 && validAttr {
+				// Store hold time reported by this node.
+				hopPayloads = append(hopPayloads, holdTime)
+
+				// Update the message.
+				msg = failData[sha256.Size:]
+			}
+
+			// Shift payloads and hmacs to the left to prepare for
+			// the next iteration.
+			o.shiftPayloadsLeft(payloads)
+			o.shiftHmacsLeft(hmacs)
+		}
 
-		// Next, we'll need to separate the data, from the MAC itself
-		// so we can reconstruct and verify it.
-		expectedMac := encryptedData[:sha256.Size]
-		data := encryptedData[sha256.Size:]
+		// Next, we'll need to separate the failure data, from the MAC
+		// itself so we can reconstruct and verify it.
+		expectedMac := failData[:sha256.Size]
+		data := failData[sha256.Size:]
 
 		// With the data split, we'll now re-generate the MAC using its
 		// specified key.
@@ -395,12 +506,55 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
 	}
 
 	return &DecryptedError{
-		SenderIdx: sender,
 		Sender:    o.circuit.PaymentPath[sender-1],
+		SenderIdx: sender,
 		Message:   msg,
+		HoldTimes: hopPayloads,
 	}, nil
 }
 
+// extractPayload extracts the payload and payload origin information from the
+// given byte slice.
+func (o *OnionErrorDecrypter) extractPayload(payloadBytes []byte) uint32 {
+	// Extract payload.
+	holdTime := binary.BigEndian.Uint32(payloadBytes[0:o.payloadLen()])
+
+	return holdTime
+}
+
+func (o *OnionErrorDecrypter) shiftPayloadsLeft(payloads []byte) {
+	copy(payloads, payloads[o.payloadLen():o.hopCount*o.payloadLen()])
+}
+
+func (o *OnionErrorDecrypter) shiftHmacsLeft(hmacs []byte) {
+	// Work from left to right to avoid overwriting data that is still
+	// needed later on in the shift operation.
+	srcIdx := o.hopCount
+	destIdx := 0
+	copyLen := o.hopCount - 1
+	for i := 0; i < o.hopCount-1; i++ {
+		// Clear first hmac slot. This slot is for the position farthest
+		// away from the error source. Because we are shifting, this
+		// cannot be relevant.
+		copy(hmacs[destIdx*o.hmacSize:], o.zeroHmac)
+
+		// The hmacs of the downstream hop become the remaining hmacs
+		// for the current hop.
+		copy(
+			hmacs[(destIdx+1)*o.hmacSize:],
+			hmacs[srcIdx*o.hmacSize:(srcIdx+copyLen)*o.hmacSize],
+		)
+
+		srcIdx += copyLen
+		destIdx += copyLen + 1
+		copyLen--
+	}
+
+	// Clear the very last hmac slot. Because we just shifted, the most
+	// downstream hop can never be the error source.
+	copy(hmacs[destIdx*o.hmacSize:], o.zeroHmac)
+}
+
 // EncryptError is used to make data obfuscation using the generated shared
 // secret.
 //
@@ -409,17 +563,144 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
 // for backward failure obfuscation of the onion failure blob. By obfuscating
 // the onion failure on every node in the path we are adding additional step of
 // the security and barrier for malware nodes to retrieve valuable information.
-// The reason for using onion obfuscation is to not give
-// away to the nodes in the payment path the information about the exact
-// failure and its origin.
-func (o *OnionErrorEncrypter) EncryptError(initial bool, data []byte) []byte {
+// The reason for using onion obfuscation is to not give away to the nodes in
+// the payment path the information about the exact failure and its origin.
+// Every node down the error path reports the recorded hold times for the HTLC,
+// so this is also passed as an argument to this function in order for this node
+// to append its own value. The attribution data is a structure which helps with
+// identifying malicious intermediate hops that may have modified the failure
+// data.
+func (o *OnionErrorEncrypter) EncryptError(initial bool, legacyData []byte,
+	attrData []byte, holdTime uint32) ([]byte, []byte, error) {
+
+	if initial && attrData != nil {
+		return nil, nil, fmt.Errorf("unable to encrypt, cannot " +
+			"initialize error with existing attribution data")
+	}
+
+	if attrData == nil {
+		attrData = o.initializePayload(holdTime)
+	}
+
 	if initial {
+		if len(legacyData) < minPaddedOnionErrorLength {
+			return nil, nil, fmt.Errorf("initial data size less "+
+				"than %v", minPaddedOnionErrorLength)
+		}
+
 		umKey := generateKey("um", &o.sharedSecret)
 		hash := hmac.New(sha256.New, umKey[:])
-		hash.Write(data)
+		hash.Write(legacyData)
 		h := hash.Sum(nil)
-		data = append(h, data...)
+		legacyData = append(h, legacyData...)
+	} else {
+		if len(attrData) < o.hmacsAndPayloadsLen() {
+			return nil, nil, ErrInvalidAttrStructure
+		}
+
+		// Add our hold time.
+		o.addIntermediatePayload(attrData, holdTime)
+
+		// Shift hmacs to create space for the new hmacs.
+		o.shiftHmacsRight(o.hmacs(attrData))
+	}
+
+	// Update hmac block.
+	o.addHmacs(attrData, legacyData)
+
+	legacy := onionEncrypt(AMMAG, &o.sharedSecret, legacyData)
+	attrError := onionEncrypt(AMMAG_EXT, &o.sharedSecret, attrData)
+
+	return legacy, attrError, nil
+}
+
+func (o *OnionErrorEncrypter) shiftHmacsRight(hmacs []byte) {
+	totalHmacs := (o.hopCount * (o.hopCount + 1)) / 2
+
+	// Work from right to left to avoid overwriting data that is still
+	// needed.
+	srcIdx := totalHmacs - 2
+	destIdx := totalHmacs - 1
+
+	// The variable copyLen contains the number of hmacs to copy for the
+	// current hop.
+	copyLen := 1
+	for i := 0; i < o.hopCount-1; i++ {
+		// Shift the hmacs to the right for the current hop. The hmac
+		// corresponding to the assumed position that is farthest away
+		// from the error source is discarded.
+		copy(
+			hmacs[destIdx*o.hmacSize:],
+			hmacs[srcIdx*o.hmacSize:(srcIdx+copyLen)*o.hmacSize],
+		)
+
+		// The number of hmacs to copy increases by one for each
+		// iteration. The further away from the error source, the more
+		// downstream hmacs exist that are relevant.
+		copyLen++
+
+		// Update indices backwards for the next iteration.
+		srcIdx -= copyLen + 1
+		destIdx -= copyLen
+	}
+
+	// Zero out the hmac slots corresponding to every possible position
+	// relative to the error source for the current hop. This is not
+	// strictly necessary as these slots are overwritten anyway, but we
+	// clear them for cleanliness.
+	for i := 0; i < o.hopCount; i++ {
+		copy(hmacs[i*o.hmacSize:], o.zeroHmac)
+	}
+}
+
+// addHmacs updates the failure data with a series of hmacs corresponding to all
+// possible positions in the path for the current node.
+func (o *OnionErrorEncrypter) addHmacs(data []byte, message []byte) {
+	payloads := o.payloads(data)
+	hmacs := o.hmacs(data)
+
+	for i := 0; i < o.hopCount; i++ {
+		position := o.hopCount - i - 1
+		hmac := o.calculateHmac(
+			o.sharedSecret, position, message, payloads, hmacs,
+		)
+
+		copy(hmacs[i*o.hmacSize:], hmac)
 	}
+}
+
+func (o *OnionErrorEncrypter) initializePayload(holdTime uint32) []byte {
+
+	// Add space for payloads and hmacs.
+	data := make([]byte, o.hmacsAndPayloadsLen())
+
+	payloads := o.payloads(data)
+
+	// Signal final hops in the payload.
+	addPayload(payloads, holdTime)
+
+	return data
+}
+
+func (o *OnionErrorEncrypter) addIntermediatePayload(data []byte,
+	holdTime uint32) {
+
+	payloads := o.payloads(data)
+
+	// Shift payloads to create space for the new payload.
+	o.shiftPayloadsRight(payloads)
+
+	// Signal intermediate hop in the payload.
+	addPayload(payloads, holdTime)
+}
+
+func (o *OnionErrorEncrypter) shiftPayloadsRight(payloads []byte) {
+	copy(payloads[o.payloadLen():], payloads)
+}
+
+func addPayload(payloads []byte, holdTime uint32) {
 
-	return onionEncrypt(&o.sharedSecret, data)
+	payload := make([]byte, 4)
+	binary.BigEndian.PutUint32(payload, holdTime)
+	copy(payloads, payload)
 }
diff --git a/error.go b/error.go
index a32c999..77a5e36 100644
--- a/error.go
+++ b/error.go
@@ -1,6 +1,8 @@
 package sphinx
 
-import "fmt"
+import (
+	"fmt"
+)
 
 var (
 	// ErrReplayedPacket is an error returned when a packet is rejected
@@ -24,4 +26,8 @@ var (
 	// ErrLogEntryNotFound is an error returned when a packet lookup in a replay
 	// log fails because it is missing.
 	ErrLogEntryNotFound = fmt.Errorf("sphinx packet is not in log")
+
+	// ErrInvalidAttrStructure is an error that signals that the provided
+	// attribution data have an invalid length.
+	ErrInvalidAttrStructure = fmt.Errorf("invalid attribution data length")
 )
diff --git a/obfuscation.go b/obfuscation.go
index a731a6c..a43f819 100644
--- a/obfuscation.go
+++ b/obfuscation.go
@@ -9,29 +9,19 @@ import (
 // OnionErrorEncrypter is a struct that's used to implement onion error
 // encryption as defined within BOLT0004.
 type OnionErrorEncrypter struct {
+	*AttrErrorStructure
 	sharedSecret Hash256
 }
 
-// NewOnionErrorEncrypter creates new instance of the onion encrypter backed by
-// the passed router, with encryption to be done using the passed ephemeralKey.
-func NewOnionErrorEncrypter(router *Router, ephemeralKey *btcec.PublicKey,
-	opts ...ProcessOnionOpt) (*OnionErrorEncrypter, error) {
-
-	cfg := &processOnionCfg{}
-	for _, o := range opts {
-		o(cfg)
-	}
-
-	sharedSecret, err := router.generateSharedSecret(
-		ephemeralKey, cfg.blindingPoint,
-	)
-	if err != nil {
-		return nil, err
-	}
+// NewOnionErrorEncrypter creates a new encrypter with the provided shared
+// secret and attributable error structure.
+func NewOnionErrorEncrypter(sharedSecret Hash256,
+	structure *AttrErrorStructure) *OnionErrorEncrypter {
 
 	return &OnionErrorEncrypter{
-		sharedSecret: sharedSecret,
-	}, nil
+		sharedSecret:       sharedSecret,
+		AttrErrorStructure: structure,
+	}
 }
 
 // Encode writes the encrypter's shared secret to the provided io.Writer.
@@ -121,12 +111,17 @@ func (c *Circuit) Encode(w io.Writer) error {
 // OnionErrorDecrypter is a struct that's used to decrypt onion errors in
 // response to failed HTLC routing attempts according to BOLT#4.
 type OnionErrorDecrypter struct {
+	*AttrErrorStructure
 	circuit *Circuit
 }
 
-// NewOnionErrorDecrypter creates new instance of onion decrypter.
-func NewOnionErrorDecrypter(circuit *Circuit) *OnionErrorDecrypter {
+// NewOnionErrorDecrypter creates new instance of onion decrypter with the
+// provided circuit and attributable error structure.
+func NewOnionErrorDecrypter(circuit *Circuit,
+	structure *AttrErrorStructure) *OnionErrorDecrypter {
+
 	return &OnionErrorDecrypter{
-		circuit: circuit,
+		circuit:            circuit,
+		AttrErrorStructure: structure,
 	}
 }
diff --git a/obfuscation_test.go b/obfuscation_test.go
index 8c0af83..a76a108 100644
--- a/obfuscation_test.go
+++ b/obfuscation_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/stretchr/testify/require"
 )
 
 // TestOnionFailure checks the ability of sender of payment to decode the
@@ -35,33 +36,42 @@ func TestOnionFailure(t *testing.T) {
 	}
 
 	// Emulate creation of the obfuscator on node where error have occurred.
-	obfuscator := &OnionErrorEncrypter{
-		sharedSecret: sharedSecrets[len(errorPath)-1],
-	}
+	obfuscator := NewOnionErrorEncrypter(
+		sharedSecrets[len(errorPath)-1], attributableErrorTestStructure,
+	)
 
 	// Emulate the situation when last hop creates the onion failure
 	// message and send it back.
-	obfuscatedData := obfuscator.EncryptError(true, failureData)
+	legacyData, _, err := obfuscator.EncryptError(
+		true, failureData, nil, 0,
+	)
+	require.NoError(t, err)
 
 	// Emulate that failure message is backward obfuscated on every hop.
 	for i := len(errorPath) - 2; i >= 0; i-- {
 		// Emulate creation of the obfuscator on forwarding node which
 		// propagates the onion failure.
-		obfuscator = &OnionErrorEncrypter{
-			sharedSecret: sharedSecrets[i],
-		}
-		obfuscatedData = obfuscator.EncryptError(false, obfuscatedData)
+		obfuscator = NewOnionErrorEncrypter(
+			sharedSecrets[i], attributableErrorTestStructure,
+		)
+
+		legacyData, _, err = obfuscator.EncryptError(
+			false, legacyData, nil, 1,
+		)
+		require.NoError(t, err)
 	}
 
 	// Emulate creation of the deobfuscator on the receiving onion error side.
 	deobfuscator := NewOnionErrorDecrypter(&Circuit{
 		SessionKey:  sessionKey,
 		PaymentPath: paymentPath,
-	})
+	}, attributableErrorTestStructure)
 
 	// Emulate that sender node receive the failure message and trying to
 	// unwrap it, by applying obfuscation and checking the hmac.
-	decryptedError, err := deobfuscator.DecryptError(obfuscatedData)
+	decryptedError, err := deobfuscator.DecryptError(
+		legacyData, nil, false,
+	)
 	if err != nil {
 		t.Fatalf("unable to de-obfuscate the onion failure: %v", err)
 	}
@@ -195,11 +205,13 @@ func TestOnionFailureSpecVector(t *testing.T) {
 		t.Fatalf("unable to get specification session key: %v", err)
 	}
 
-	var obfuscatedData []byte
 	sharedSecrets, _, err := generateSharedSecrets(paymentPath, sessionKey)
 	if err != nil {
 		t.Fatalf("Unexpected error while generating secrets: %v", err)
 	}
+
+	var legacyData, attrData []byte
+
 	for i, test := range onionErrorData {
 		// Decode the shared secret and check that it matchs with
 		// specification.
@@ -208,16 +220,17 @@ func TestOnionFailureSpecVector(t *testing.T) {
 			t.Fatalf("unable to decode spec shared secret: %v",
 				err)
 		}
-		obfuscator := &OnionErrorEncrypter{
-			sharedSecret: sharedSecrets[len(sharedSecrets)-1-i],
-		}
+		obfuscator := NewOnionErrorEncrypter(
+			sharedSecrets[len(sharedSecrets)-1-i],
+			attributableErrorTestStructure,
+		)
 
 		var b bytes.Buffer
 		if err := obfuscator.Encode(&b); err != nil {
 			t.Fatalf("unable to encode obfuscator: %v", err)
 		}
 
-		obfuscator2 := &OnionErrorEncrypter{}
+		obfuscator2 := NewOnionErrorEncrypter(Hash256{}, attributableErrorTestStructure)
 		obfuscatorReader := bytes.NewReader(b.Bytes())
 		if err := obfuscator2.Decode(obfuscatorReader); err != nil {
 			t.Fatalf("unable to decode obfuscator: %v", err)
@@ -236,11 +249,13 @@ func TestOnionFailureSpecVector(t *testing.T) {
 		if i == 0 {
 			// Emulate the situation when last hop creates the onion failure
 			// message and send it back.
-			obfuscatedData = obfuscator.EncryptError(true, failureData)
+			legacyData, attrData, err = obfuscator.EncryptError(true, failureData, nil, 0)
+			require.NoError(t, err)
 		} else {
 			// Emulate the situation when forward node obfuscates
 			// the onion failure.
-			obfuscatedData = obfuscator.EncryptError(false, obfuscatedData)
+			legacyData, attrData, err = obfuscator.EncryptError(false, legacyData, attrData, 0)
+			require.NoError(t, err)
 		}
 
 		// Decode the obfuscated data and check that it matches the
@@ -250,21 +265,24 @@ func TestOnionFailureSpecVector(t *testing.T) {
 			t.Fatalf("unable to decode spec obfusacted "+
 				"data: %v", err)
 		}
-		if !bytes.Equal(expectedEncryptErrordData, obfuscatedData) {
+		if !bytes.Equal(expectedEncryptErrordData, legacyData) {
 			t.Fatalf("obfuscated data not match spec: expected %x, "+
 				"got %x", expectedEncryptErrordData[:],
-				obfuscatedData[:])
+				legacyData[:])
 		}
 	}
 
 	deobfuscator := NewOnionErrorDecrypter(&Circuit{
 		SessionKey:  sessionKey,
 		PaymentPath: paymentPath,
-	})
+	}, attributableErrorTestStructure,
+	)
 
 	// Emulate that sender node receives the failure message and trying to
 	// unwrap it, by applying obfuscation and checking the hmac.
-	decryptedError, err := deobfuscator.DecryptError(obfuscatedData)
+	decryptedError, err := deobfuscator.DecryptError(
+		legacyData, attrData, false,
+	)
 	if err != nil {
 		t.Fatalf("unable to de-obfuscate the onion failure: %v", err)
 	}
diff --git a/sphinx.go b/sphinx.go
index 8e16b23..423082e 100644
--- a/sphinx.go
+++ b/sphinx.go
@@ -546,7 +546,7 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte,
 	}
 
 	// Compute the shared secret for this onion packet.
-	sharedSecret, err := r.generateSharedSecret(
+	sharedSecret, err := r.GenerateSharedSecret(
 		onionPkt.EphemeralKey, cfg.blindingPoint,
 	)
 	if err != nil {
@@ -587,7 +587,7 @@ func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, assocData []byte,
 	}
 
 	// Compute the shared secret for this onion packet.
-	sharedSecret, err := r.generateSharedSecret(
+	sharedSecret, err := r.GenerateSharedSecret(
 		onionPkt.EphemeralKey, cfg.blindingPoint,
 	)
 	if err != nil {
@@ -780,7 +780,7 @@ func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket,
 	}
 
 	// Compute the shared secret for this onion packet.
-	sharedSecret, err := t.router.generateSharedSecret(
+	sharedSecret, err := t.router.GenerateSharedSecret(
 		onionPkt.EphemeralKey, cfg.blindingPoint,
 	)
 	if err != nil {
diff --git a/testdata/onion-test.json b/testdata/onion-test.json
index 193256f..5721d3e 100644
--- a/testdata/onion-test.json
+++ b/testdata/onion-test.json
@@ -1,28 +1,39 @@
 {
   "comment": "A testcase for a variable length hop_payload. The third payload is 275 bytes long.",
   "generate": {
+    "encodedFailureMessage": "0140400f0000000000000064000c3500fd84d1fd012c80808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808002c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
     "session_key": "4141414141414141414141414141414141414141414141414141414141414141",
     "associated_data": "4242424242424242424242424242424242424242424242424242424242424242",
     "hops": [
       {
         "pubkey": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619",
-        "payload": "1202023a98040205dc06080000000000000001"
+        "payload": "1202023a98040205dc06080000000000000001",
+        "sharedSecret": "b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328",
+        "encryptedMessage": "d77d0711b5f71d1d1be56bd88b3bb7ebc1792bb739ea7ebc1bc3b031b8bc2df3a50e25aeb99f47d7f7ab39e24187d3f4df9c4333463b053832ee9ac07274a5261b8b2a01fc09ce9ea7cd04d7b585dfb8cf5958e3f3f2a4365d1ec0df1d83c6a6221b5b7d1ff30156a2289a1d3ee559e7c7256bda444bb8e046f860e00b3a59a85e1e1a43de215fd5e6bf646a5deab97b1912c934e31b1cfd344764d6ca7e14ea7b3f2a951aba907c964c0f5d19a44e6d1d7279637321fa598adde927b3087d238f8b426ecde500d318617cdb7a56e6ce3520fc95be41a549973764e4dc483853ecc313947709f1b5199cb077d46e701fa633e11d3e13b03e9212c115ca6fa004b2f3dd912814693b705a561a06da54cdf603677a3abecdc22c7358c2de3cef771b366a568150aeecc86ad1990bb0f4e2865933b03ea0df87901bff467908273dc6cea31cbab0e2b8d398d10b001058c259ed221b7b55762f4c7e49c8c11a45a107b7a2c605c26dc5b0b10d719b1c844670102b2b6a36c43fe4753a78a483fc39166ae28420f112d50c10ee64ca69569a2f690712905236b7c2cb7ac8954f02922d2d918c56d42649261593c47b14b324a65038c3c5be8d3c403ce0c8f19299b1664bf077d7cf1636c4fb9685a8e58b7029fd0939fa07925a60bed339b23f973293598f595e75c8f9d455d7cebe4b5e23357c8bd47d66d6628b39427e37e0aecbabf46c11be6771f7136e108a143ae9bafba0fc47a51b6c7deef4cba54bae906398ee3162a41f2191ca386b628bde7e1dd63d1611aa01a95c456df337c763cb8c3a81a6013aa633739d8cd554c688102211725e6adad165adc1bcd429d020c51b4b25d2117e8bb27eb0cc7020f9070d4ad19ac31a76ebdf5f9246646aeadbfb9a3f1d75bd8237961e786302516a1a781780e8b73f58dc06f307e58bd0eb1d8f5c9111f01312974c1dc777a6a2d3834d8a2a40014e9818d0685cb3919f6b3b788ddc640b0ff9b1854d7098c7dd6f35196e902b26709640bc87935a3914869a807e8339281e9cedaaca99474c3e7bdd35050bb998ab4546f9900904e0e39135e861ff7862049269701081ebce32e4cca992c6967ff0fd239e38233eaf614af31e186635e9439ec5884d798f9174da6ff569d68ed5c092b78bd3f880f5e88a7a8ab36789e1b57b035fb6c32a6358f51f83e4e5f46220bcad072943df8bd9541a61b7dae8f30fa3dd5fb39b1fd9a0b8e802552b78d4ec306ecee15bfe6da14b29ba6d19ce5be4dd478bca74a52429cd5309d404655c3dec85c252"
       },
       {
         "pubkey": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c",
-        "payload": "52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f"
+        "payload": "52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f",
+        "sharedSecret": "21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d",
+        "encryptedMessage": "1571e10db7f8aa9f8e7e99caaf9c892e106c817df1d8e3b7b0e39d1c48f631e473e17e205489dd7b3c634cac3be0825cbf01418cd46e83c24b8d9c207742db9a0f0e5bcd888086498159f08080ba7bf36dee297079eb841391ccd3096da76461e314863b6412efe0ffe228d51c6097db10d3edb2e50ea679820613bfe9db11ba02920ab4c1f2a79890d997f1fc022f3ab78f0029cc6de0c90be74d55f4a99bf77a50e20f8d076fe61776190a61d2f41c408871c0279309cba3b60fcdc7efc4a0e90b47cb4a418fc78f362ecc7f15ebbce9f854c09c7be300ebc1a40a69d4c7cb7a19779b6905e82bec221a709c1dab8cbdcde7b527aca3f54bde651aa9f3f2178829cee3f1c0b9292758a40cc63bd998fcd0d3ed4bdcaf1023267b8f8e44130a63ad15f76145936552381eabb6d684c0a3af6ba8efcf207cebaea5b7acdbb63f8e7221102409d10c23f0514dc9f4d0efb2264161a193a999a23e992632710580a0d320f676d367b9190721194514457761af05207cdab2b6328b1b3767eacb36a7ef4f7bd2e16762d13df188e0898b7410f62459458712a44bf594ae662fd89eb300abb6952ff8ad40164f2bcd7f86db5c7650b654b79046de55d51aa8061ce35f867a3e8f5bf98ad920be827101c64fb871d86e53a4b3c0455bfac5784168218aa72cbee86d9c750a9fa63c363a8b43d7bf4b2762516706a306f0aa3be1ec788b5e13f8b24837e53ac414f211e11c7a093cd9653dfa5fba4e377c79adfa5e841e2ddb6afc054fc715c05ddc6c8fc3e1ee3406e1ffceb2df77dc2f02652614d1bfcfaddebaa53ba919c7051034e2c7b7cfaabdf89f26e7f8e3f956d205dfab747ad0cb505b85b54a68439621b25832cbc2898919d0cd7c0a64cfd235388982dd4dd68240cb668f57e1d2619a656ed326f8c92357ee0d9acead3c20008bc5f04ca8059b55d77861c6d04dfc57cfba57315075acbe1451c96cf28e1e328e142890248d18f53b5d3513ce574dea7156cf596fdb3d909095ec287651f9cf1bcdc791c5938a5dd9b47e84c004d24ab3ae74492c7e8dcc1da15f65324be2672947ec82074cac8ce2b925bc555facbbf1b55d63ea6fbea6a785c97d4caf2e1dad9551b7f66c31caae5ebc7c0047e892f201308fcf452c588be0e63d89152113d87bf0dbd01603b4cdc7f0b724b0714a9851887a01f709408882e18230fe810b9fafa58a666654576d8eba3005f07221f55a6193815a672e5db56204053bc4286fa3db38250396309fd28011b5708a26a2d76c4a333b69b6bfd272fb"
       },
       {
         "pubkey": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007",
-        "payload": "12020230d4040204e206080000000000000003"
+        "payload": "12020230d4040204e206080000000000000003",
+        "sharedSecret": "3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc",
+        "encryptedMessage": "34e34397b8621ec2f2b54dbe6c14073e267324cd60b152bce76aec8729a6ddefb61bc263be4b57bd592aae604a32bea69afe6ef4a6b573c26b17d69381ec1fc9b5aa769d148f2f1f8b5377a73840bb6dc641f68e356323d766fff0aaca5039fe7fc27038195844951a97d5a5b26698a4ca1e9cd4bca1fcca0aac5fee91b18977d2ad0e399ba159733fc98f6e96898ebc39bf0028c9c81619233bab6fad0328aa183a635fac20437fa6e00e899b2527c3697a8ab7342e42d55a679b176ab76671fcd480a9894cb897fa6af0a45b917a162bed6c491972403185df7235502f7ada65769d1bfb12d29f10e25b0d3cc08bbf6de8481ac5c04df32b4533b4f764c2aefb7333202645a629fb16e4a208e9045dc36830759c852b31dd613d8b2b10bbead1ed4eb60c85e8a4517deba5ab53e39867c83c26802beee2ee545bdd713208751added5fc0eb2bc89a5aa2decb18ee37dac39f22a33b60cc1a369d24de9f3d2d8b63c039e248806de4e36a47c7a0aed30edd30c3d62debdf1ad82bf7aedd7edec413850d91c261e12beec7ad1586a9ad25b2db62c58ca17119d61dcc4f3e5c4520c42a8e384a45d8659b338b3a08f9e123a1d3781f5fc97564ccff2c1d97f06fa0150cfa1e20eacabefb0c339ec109336d207cc63d9170752fc58314c43e6d4a528fd0975afa85f3aa186ff1b6b8cb12c97ed4ace295b0ef5f075f0217665b8bb180246b87982d10f43c9866b22878106f5214e99188781180478b07764a5e12876ddcb709e0a0a8dd42cf004c695c6fc1669a6fd0e4a1ca54b024d0d80eac492a9e5036501f36fb25b72a054189294955830e43c18e55668337c8c6733abb09fc2d4ade18d5a853a2b82f7b4d77151a64985004f1d9218f2945b63c56fdebd1e96a2a7e49fa70acb4c39873947b83c191c10e9a8f40f60f3ad5a2be47145c22ea59ed3f5f4e61cb069e875fb67142d281d784bf925cc286eacc2c43e94d08da4924b83e58dbf2e43fa625bdd620eba6d9ce960ff17d14ed1f2dbee7d08eceb540fdc75ff06dabc767267658fad8ce99e2a3236e46d2deedcb51c3c6f81589357edebac9772a70b3d910d83cd1b9ce6534a011e9fa557b891a23b5d88afcc0d9856c6dabeab25eea55e9a248182229e4927f268fe5431672fcce52f434ca3d27d1a2136bae5770bb36920df12fbc01d0e8165610efa04794f414c1417f1d4059435c5385bfe2de83ce0e238d6fd2dbd3c0487c69843298577bfa480fe2a16ab2a0e4bc712cd8b5a14871cda61c993b6835303d9043d7689a"
       },
       {
         "pubkey": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991",
-        "payload": "1202022710040203e806080000000000000004"
+        "payload": "1202022710040203e806080000000000000004",
+        "sharedSecret": "a6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae",
+        "encryptedMessage": "74a4ea61339463642a2182758871b2ea724f31f531aa98d80f1c3043febca41d5ee52e8b1e127e61719a0d078db8909748d57839e58424b91f063c4fbc8a221bef261140e66a9b596ca6d420a973ad54fef30646ae53ccf0855b61f291a81e0ec6dc0f6bf69f0ca0e5889b7e23f577ba67d2a7d6a2aa91264ab9b20630ed52f8ed56cc10a869807cd1a4c2cd802d8433fee5685d6a04edb0bff248a480b93b01904bed3bb31705d1ecb7332004290cc0cd9cc2f7907cf9db28eec02985301668f53fbc28c3e095c8f3a6cd8cab28e5e442fd9ba608b8b12e098731bbfda755393bd403c62289093b40390b2bae337fc87d2606ca028311d73a9ffbdffef56020c735ada30f54e577c6a9ec515ae2739290609503404b118d7494499ecf0457d75015bb60a16288a4959d74cf5ac5d8d6c113de39f748a418d2a7083b90c9c0a09a49149fd1f2d2cde4412e5aa2421eca6fd4f6fe6b2c362ff37d1a0608c931c7ca3b8fefcfd4c44ef9c38357a0767b14f83cb49bd1989fb3f8e2ab202ac98bd8439790764a40bf309ea2205c1632610956495720030a25dc7118e0c868fdfa78c3e9ecce58215579a0581b3bafdb7dbbe53be9e904567fdc0ce1236aab5d22f1ebc18997e3ea83d362d891e04c5785fd5238326f767bce499209f8db211a50e1402160486e98e7235cf397dbb9ae19fd9b79ef589c821c6f99f28be33452405a003b33f4540fe0a41dfcc286f4d7cc10b70552ba7850869abadcd4bb7f256823face853633d6e2a999ac9fcd259c71d08e266db5d744e1909a62c0db673745ad9585949d108ab96640d2bc27fb4acac7fa8b170a30055a5ede90e004df9a44bdc29aeb4a6bec1e85dde1de6aaf01c6a5d12405d0bec22f49026cb23264f8c04b8401d3c2ab6f2e109948b6193b3bec27adfe19fb8afb8a92364d6fc5b219e8737d583e7ff3a4bcb75d53edda3bf3f52896ac36d8a877ad9f296ea6c045603fc62ac4ae41272bde85ef7c3b3fd3538aacfd5b025fefbe277c2906821ecb20e6f75ea479fa3280f9100fb0089203455c56b6bc775e5c2f0f58c63edd63fa3eec0b40da4b276d0d41da2ec0ead865a98d12bc694e23d8eaadd2b4d0ee88e9570c88fb878930f492e036d27998d593e47763927ff7eb80b188864a3846dd2238f7f95f4090ed399ae95deaeb37abca1cf37c397cc12189affb42dca46b4ff6988eb8c060691d155302d448f50ff70a794d97c0408f8cee9385d6a71fa412e36edcb22dbf433db9db4779f27b682ee17fc05e70c8e794b9f7f6d1"
       },
       {
         "pubkey": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145",
-        "payload": "fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"
+        "payload": "fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a",
+        "sharedSecret": "53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66",
+        "encryptedMessage": "84986c936d26bfd3bb2d34d3ec62cfdb63e0032fdb3d9d75f3e5d456f73dffa7e35aab1db4f1bd3b98ff585caf004f656c51037a3f4e810d275f3f6aea0c8e3a125ebee5f374b6440bcb9bb2955ebf706f42be9999a62ed49c7a81fc73c0b4a16419fd6d334532f40bf179dd19afec21bd8519d5e6ebc3802501ef373bc378eee1f14a6fc5fab5b697c91ce31d5922199d1b0ad5ee12176aacafc7c81d54bc5b8fb7e63f3bfd40a3b6e21f985340cbd1c124c7f85f0369d1aa86ebc66def417107a7861131c8bcd73e8946f4fb54bfac87a2dc15bd7af642f32ae583646141e8875ef81ec9083d7e32d5f135131eab7a43803360434100ff67087762bbe3d6afe2034f5746b8c50e0c3c20dd62a4c174c38b1df7365dccebc7f24f19406649fbf48981448abe5c858bbd4bef6eb983ae7a23e9309fb33b5e7c0522554e88ca04b1d65fc190947dead8c0ccd32932976537d869b5ca53ed4945bccafab2a014ea4cbdc6b0250b25be66ba0afff2ff19c0058c68344fd1b9c472567147525b13b1bc27563e61310110935cf89fda0e34d0575e2389d57bdf2869398ca2965f64a6f04e1d1c2edf2082b97054264a47824dd1a9691c27902b39d57ae4a94dd6481954a9bd1b5cff4ab29ca221fa2bf9b28a362c9661206f896fc7cec563fb80aa5eaccb26c09fa4ef7a981e63028a9c4dac12f82ccb5bea090d56bbb1a4c431e315d9a169299224a8dbd099fb67ea61dfc604edf8a18ee742550b636836bb552dabb28820221bf8546331f32b0c143c1c89310c4fa2e1e0e895ce1a1eb0f43278fdb528131a3e32bfffe0c6de9006418f5309cba773ca38b6ad8507cc59445ccc0257506ebc16a4c01d4cd97e03fcf7a2049fea0db28447858f73b8e9fe98b391b136c9dc510288630a1f0af93b26a8891b857bfe4b818af99a1e011e6dbaa53982d29cf74ae7dffef45545279f19931708ed3eede5e82280eab908e8eb80abff3f1f023ab66869297b40da8496861dc455ac3abe1efa8a6f9e2c4eda48025d43a486a3f26f269743eaa30d6f0e1f48db6287751358a41f5b07aee0f098862e3493731fe2697acce734f004907c6f11eef189424fee52cd30ad708707eaf2e441f52bcf3d0c5440c1742458653c0c8a27b5ade784d9e09c8b47f1671901a29360e7e5e94946b9c75752a1a8d599d2a3e14ac81b84d42115cd688c8383a64fc6e7e1dc5568bb4837358ebe63207a4067af66b2027ad2ce8fb7ae3a452d40723a51fdf9f9c9913e8029a222cf81d12ad41e58860d75deb6de30ad"
       }
     ]
   },