From b0bf831fe4bf656af13f2f139668ff294aef94c0 Mon Sep 17 00:00:00 2001
From: Joost Jager <joost.jager@gmail.com>
Date: Tue, 23 May 2023 14:52:52 +0200
Subject: [PATCH 1/4] add minPaddedOnionErrorLength constant

---
 crypto.go | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/crypto.go b/crypto.go
index 0930544..b4ae481 100644
--- a/crypto.go
+++ b/crypto.go
@@ -61,8 +61,8 @@ func (p *PrivKeyECDH) PubKey() *btcec.PublicKey {
 // k is our private key, and P is the public key, we perform the following
 // operation:
 //
-//  sx := k*P
-//  s := sha256(sx.SerializeCompressed())
+//	sx := k*P
+//	s := sha256(sx.SerializeCompressed())
 //
 // NOTE: This is part of the SingleKeyECDH interface.
 func (p *PrivKeyECDH) ECDH(pub *btcec.PublicKey) ([32]byte, error) {
@@ -235,10 +235,14 @@ func onionEncrypt(sharedSecret *Hash256, data []byte) []byte {
 	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

From 7b827337175239c32b5b8df5d162654a298939d4 Mon Sep 17 00:00:00 2001
From: Joost Jager <joost.jager@gmail.com>
Date: Tue, 17 Jan 2023 13:44:32 +0100
Subject: [PATCH 2/4] initialize NewOnionErrorEncrypter with shared secret
 directly

Allow for more flexible usage of the error encrypter. This is useful when upgrading an
existing legacy error encrypter to fat errors in lnd.
---
 obfuscation.go      | 11 ++---------
 obfuscation_test.go | 16 ++++++----------
 2 files changed, 8 insertions(+), 19 deletions(-)

diff --git a/obfuscation.go b/obfuscation.go
index b8df7cc..14aabbe 100644
--- a/obfuscation.go
+++ b/obfuscation.go
@@ -15,17 +15,10 @@ type OnionErrorEncrypter struct {
 // NewOnionErrorEncrypter creates new instance of the onion encrypter backed by
 // the passed router, with encryption to be doing using the passed
 // ephemeralKey.
-func NewOnionErrorEncrypter(router *Router,
-	ephemeralKey *btcec.PublicKey) (*OnionErrorEncrypter, error) {
-
-	sharedSecret, err := router.generateSharedSecret(ephemeralKey)
-	if err != nil {
-		return nil, err
-	}
-
+func NewOnionErrorEncrypter(sharedSecret Hash256) *OnionErrorEncrypter {
 	return &OnionErrorEncrypter{
 		sharedSecret: sharedSecret,
-	}, nil
+	}
 }
 
 // Encode writes the encrypter's shared secret to the provided io.Writer.
diff --git a/obfuscation_test.go b/obfuscation_test.go
index da62ed0..d8f3cbd 100644
--- a/obfuscation_test.go
+++ b/obfuscation_test.go
@@ -35,9 +35,7 @@ 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])
 
 	// Emulate the situation when last hop creates the onion failure
 	// message and send it back.
@@ -47,9 +45,7 @@ func TestOnionFailure(t *testing.T) {
 	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],
-		}
+		obfuscator = NewOnionErrorEncrypter(sharedSecrets[i])
 		obfuscatedData = obfuscator.EncryptError(false, obfuscatedData)
 	}
 
@@ -208,16 +204,16 @@ 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],
+		)
 
 		var b bytes.Buffer
 		if err := obfuscator.Encode(&b); err != nil {
 			t.Fatalf("unable to encode obfuscator: %v", err)
 		}
 
-		obfuscator2 := &OnionErrorEncrypter{}
+		obfuscator2 := NewOnionErrorEncrypter(Hash256{})
 		obfuscatorReader := bytes.NewReader(b.Bytes())
 		if err := obfuscator2.Decode(obfuscatorReader); err != nil {
 			t.Fatalf("unable to decode obfuscator: %v", err)

From 78880cd09c878c645fe8bcaae13f4564bec411ac Mon Sep 17 00:00:00 2001
From: Joost Jager <joost.jager@gmail.com>
Date: Tue, 17 Jan 2023 13:51:08 +0100
Subject: [PATCH 3/4] export GenerateSharedSecret

---
 crypto.go | 8 ++++----
 sphinx.go | 6 +++---
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/crypto.go b/crypto.go
index b4ae481..16acc66 100644
--- a/crypto.go
+++ b/crypto.go
@@ -204,13 +204,13 @@ func blindBaseElement(blindingFactor btcec.ModNScalar) *btcec.PublicKey {
 //
 // 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 by given ephemeral key.
-func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) {
+// GenerateSharedSecret generates the shared secret by given ephemeral key.
+func (r *Router) GenerateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) {
 	var sharedSecret Hash256
 
 	// Ensure that the public key is on our curve.
diff --git a/sphinx.go b/sphinx.go
index 36e9a81..d59d0e1 100644
--- a/sphinx.go
+++ b/sphinx.go
@@ -534,7 +534,7 @@ 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)
+	sharedSecret, err := r.GenerateSharedSecret(onionPkt.EphemeralKey)
 	if err != nil {
 		return nil, err
 	}
@@ -568,7 +568,7 @@ func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket,
 	assocData []byte) (*ProcessedPacket, error) {
 
 	// Compute the shared secret for this onion packet.
-	sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey)
+	sharedSecret, err := r.GenerateSharedSecret(onionPkt.EphemeralKey)
 	if err != nil {
 		return nil, err
 	}
@@ -731,7 +731,7 @@ 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(
+	sharedSecret, err := t.router.GenerateSharedSecret(
 		onionPkt.EphemeralKey,
 	)
 	if err != nil {

From 787ad3d102b0b71bd7700b1c5a2263bdfa249f8f Mon Sep 17 00:00:00 2001
From: Joost Jager <joost.jager@gmail.com>
Date: Thu, 10 Nov 2022 10:08:53 +0100
Subject: [PATCH 4/4] add attributable error encryption and decryption

---
 attributable_error_crypto.go      | 170 ++++++++++++
 attributable_error_crypto_test.go | 432 ++++++++++++++++++++++++++++++
 attributable_error_decrypt.go     | 211 +++++++++++++++
 attributable_error_encrypt.go     | 171 ++++++++++++
 go.mod                            |   1 +
 go.sum                            |  14 +
 testdata/attributable_error.json  |  25 ++
 7 files changed, 1024 insertions(+)
 create mode 100644 attributable_error_crypto.go
 create mode 100644 attributable_error_crypto_test.go
 create mode 100644 attributable_error_decrypt.go
 create mode 100644 attributable_error_encrypt.go
 create mode 100644 testdata/attributable_error.json

diff --git a/attributable_error_crypto.go b/attributable_error_crypto.go
new file mode 100644
index 0000000..1aad840
--- /dev/null
+++ b/attributable_error_crypto.go
@@ -0,0 +1,170 @@
+package sphinx
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"io"
+)
+
+type payloadSource byte
+
+const (
+	// payloadIntermediateNode is a marker to signal that this attributable
+	// error payload is originating from a node between the payer and the
+	// error source.
+	payloadIntermediateNode payloadSource = 0
+
+	// payloadErrorNode is a marker to signal that this attributable error
+	// payload is originating from the error source.
+	payloadErrorNode payloadSource = 1
+)
+
+// 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
+}
+
+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 1 + o.fixedPayloadLen
+}
+
+// message returns a slice containing the message in the given failure data
+// block. The message is positioned at the beginning of the block.
+func (o *AttrErrorStructure) message(data []byte) []byte {
+	return data[:len(data)-o.hmacsAndPayloadsLen()]
+}
+
+// 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/attributable_error_crypto_test.go b/attributable_error_crypto_test.go
new file mode 100644
index 0000000..8b22264
--- /dev/null
+++ b/attributable_error_crypto_test.go
@@ -0,0 +1,432 @@
+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) {
+	t.Parallel()
+
+	t.Run("32 byte hmac", func(t *testing.T) { testAttributableOnionFailure(t, 32) })
+	t.Run("4 byte hmac", func(t *testing.T) { testAttributableOnionFailure(t, 4) })
+}
+
+// TestAttributableOnionFailure checks the ability of sender of payment to
+// decode the obfuscated onion error.
+func testAttributableOnionFailure(t *testing.T, hmacBytes int) {
+	t.Parallel()
+
+	var structure = NewAttrErrorStructure(27, 8, hmacBytes)
+
+	// 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 := NewOnionAttrErrorEncrypter(
+		sharedSecrets[len(errorPath)-1], structure,
+	)
+
+	// Emulate the situation when last hop creates the onion failure
+	// message and send it back.
+	finalPayload := [8]byte{1}
+	obfuscatedData, err := obfuscator.EncryptError(
+		true, failureData, finalPayload[:],
+	)
+	require.NoError(t, err)
+	payloads := [][]byte{finalPayload[:]}
+
+	// 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 = NewOnionAttrErrorEncrypter(
+			sharedSecrets[i], structure,
+		)
+
+		intermediatePayload := [8]byte{byte(100 + i)}
+		obfuscatedData, err = obfuscator.EncryptError(
+			false, obfuscatedData, intermediatePayload[:],
+		)
+		require.NoError(t, err)
+
+		payloads = append([][]byte{intermediatePayload[:]}, payloads...)
+	}
+
+	// Emulate creation of the deobfuscator on the receiving onion error
+	// side.
+	deobfuscator := NewOnionAttrErrorDecrypter(&Circuit{
+		SessionKey:  sessionKey,
+		PaymentPath: paymentPath,
+	}, structure)
+
+	// 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)
+	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.Payloads)
+}
+
+// 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 := NewOnionAttrErrorEncrypter(
+		sharedSecrets[len(errorPath)-1], attributableErrorTestStructure,
+	)
+
+	// Emulate the situation when last hop creates the onion failure
+	// message and send it back.
+	payload := [4]byte{1}
+	obfuscatedData, err := obfuscator.EncryptError(
+		true, failureData, payload[:],
+	)
+	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 = NewOnionAttrErrorEncrypter(
+			sharedSecrets[i], attributableErrorTestStructure,
+		)
+
+		payload := [4]byte{byte(100 + i)}
+		obfuscatedData, err = obfuscator.EncryptError(
+			false, obfuscatedData, payload[:],
+		)
+		require.NoError(t, err)
+
+		// Hop 1 (the second hop from the sender pov) is corrupting the
+		// failure message.
+		if i == 1 {
+			obfuscatedData[0] ^= 255
+		}
+	}
+
+	// Emulate creation of the deobfuscator on the receiving onion error
+	// side.
+	deobfuscator := NewOnionAttrErrorDecrypter(&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)
+	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 specVector struct {
+	EncodedFailureMessage string `json:"encodedFailureMessage"`
+
+	Hops []specHop `json:"hops"`
+}
+
+// TestOnionFailureSpecVector checks that onion error corresponds to the
+// specification.
+func TestAttributableFailureSpecVector(t *testing.T) {
+	t.Parallel()
+
+	vectorBytes, err := os.ReadFile("testdata/attributable_error.json")
+	require.NoError(t, err)
+
+	var vector specVector
+	require.NoError(t, json.Unmarshal(vectorBytes, &vector))
+
+	failureData, err := hex.DecodeString(vector.EncodedFailureMessage)
+	require.NoError(t, err)
+
+	paymentPath, err := getSpecPubKeys()
+	require.NoError(t, err)
+
+	sessionKey, err := getSpecSessionKey()
+	require.NoError(t, err)
+
+	var obfuscatedData []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 := NewOnionAttrErrorEncrypter(
+			sharedSecrets[len(sharedSecrets)-1-i],
+			attributableErrorTestStructure,
+		)
+
+		require.Equal(
+			t, expectedSharedSecret, obfuscator.sharedSecret[:],
+		)
+
+		payload := [4]byte{0, 0, 0, byte(i + 1)}
+
+		if i == 0 {
+			// Emulate the situation when last hop creates the onion
+			// failure message and send it back.
+			obfuscatedData, err = obfuscator.EncryptError(
+				true, failureData, payload[:],
+			)
+			require.NoError(t, err)
+		} else {
+			// Emulate the situation when forward node obfuscates
+			// the onion failure.
+			obfuscatedData, err = obfuscator.EncryptError(
+				false, obfuscatedData, payload[:],
+			)
+			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, obfuscatedData)
+	}
+
+	deobfuscator := NewOnionAttrErrorDecrypter(&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)
+	require.NoError(t, err)
+
+	// Check that message have been properly de-obfuscated.
+	require.Equal(t, decryptedError.Message, failureData)
+
+	// We should understand the node from which error have been received.
+	require.Equal(t,
+		decryptedError.Sender.SerializeCompressed(),
+		paymentPath[len(paymentPath)-1].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 := NewOnionAttrErrorDecrypter(&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)
+	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 := NewOnionAttrErrorDecrypter(&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)
+
+	decryptedError, err := deobfuscator.DecryptError(obfuscatedData)
+	require.NoError(t, err)
+
+	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 := NewOnionAttrErrorEncrypter(
+		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 := NewOnionAttrErrorDecrypter(
+		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/attributable_error_decrypt.go b/attributable_error_decrypt.go
new file mode 100644
index 0000000..d85da4f
--- /dev/null
+++ b/attributable_error_decrypt.go
@@ -0,0 +1,211 @@
+package sphinx
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+)
+
+// DecryptedAttrError contains the decrypted attributable error message
+// and its sender.
+type DecryptedAttrError struct {
+	DecryptedError
+
+	// Payloads is an array of data blocks reported by each node on the
+	// (error) path.
+	Payloads [][]byte
+}
+
+// OnionAttrErrorDecrypter is a struct that's used to decrypt
+// attributable onion errors in response to failed HTLC routing attempts
+// according to BOLT#4.
+type OnionAttrErrorDecrypter struct {
+	AttrErrorStructure
+
+	circuit *Circuit
+}
+
+// NewOnionAttrErrorDecrypter creates new instance of an attributable
+// error onion decrypter.
+func NewOnionAttrErrorDecrypter(circuit *Circuit,
+	structure *AttrErrorStructure) *OnionAttrErrorDecrypter {
+
+	return &OnionAttrErrorDecrypter{
+		AttrErrorStructure: *structure,
+		circuit:            circuit,
+	}
+}
+
+// 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 *OnionAttrErrorDecrypter) DecryptError(encryptedData []byte) (
+	*DecryptedAttrError, error) {
+
+	// Ensure the error message length is enough to contain the payloads and
+	// hmacs blocks. Otherwise blame the first hop.
+	if len(encryptedData) <
+		minPaddedOnionErrorLength+o.hmacsAndPayloadsLen() {
+
+		return &DecryptedAttrError{
+			DecryptedError: DecryptedError{
+				SenderIdx: 1,
+				Sender:    o.circuit.PaymentPath[0],
+			},
+		}, nil
+	}
+
+	sharedSecrets, err := generateSharedSecrets(
+		o.circuit.PaymentPath,
+		o.circuit.SessionKey,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error generating shared secret: "+
+			"%w", err)
+	}
+
+	var (
+		sender      int
+		msg         []byte
+		dummySecret Hash256
+	)
+	copy(dummySecret[:], bytes.Repeat([]byte{1}, 32))
+
+	// 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.
+	hopPayloads := make([][]byte, 0)
+	for i := 0; i < o.hopCount; i++ {
+		var sharedSecret Hash256
+
+		// If we've already found the sender, then we'll use our dummy
+		// secret to continue decryption attempts to fill out the rest
+		// of the loop. Otherwise, we'll use the next shared secret in
+		// line.
+		if sender != 0 || i > len(sharedSecrets)-1 {
+			sharedSecret = dummySecret
+		} else {
+			sharedSecret = sharedSecrets[i]
+		}
+
+		// With the shared secret, we'll now strip off a layer of
+		// encryption from the encrypted error payload.
+		encryptedData = onionEncrypt(&sharedSecret, encryptedData)
+
+		message := o.message(encryptedData)
+		payloads := o.payloads(encryptedData)
+		hmacs := o.hmacs(encryptedData)
+
+		position := o.hopCount - i - 1
+		expectedHmac := o.calculateHmac(
+			sharedSecret, position, message, payloads, hmacs,
+		)
+		actualHmac := hmacs[i*o.hmacSize : (i+1)*o.hmacSize]
+
+		// If the hmac does not match up, exit with a nil message but
+		// only after finishing the constant number of iterations.
+		if !bytes.Equal(actualHmac, expectedHmac) && sender == 0 {
+			sender = i + 1
+			msg = nil
+		}
+
+		// Extract the payload and exit with a nil message if it is
+		// invalid.
+		source, payload, err := o.extractPayload(payloads)
+		if sender == 0 {
+			if err != nil {
+				sender = i + 1
+				msg = nil
+			}
+
+			// Store data reported by this node.
+			hopPayloads = append(hopPayloads, payload)
+
+			// If we are at the node that is the source of the
+			// error, we can now save the message in our return
+			// variable.
+			if source == payloadErrorNode {
+				sender = i + 1
+				msg = message
+			}
+		}
+
+		// Shift payloads and hmacs to the left to prepare for the next
+		// iteration.
+		o.shiftPayloadsLeft(payloads)
+		o.shiftHmacsLeft(hmacs)
+	}
+
+	// If the sender index is still zero, all hmacs checked out but none of
+	// the payloads was a final payload. In this case we must be dealing
+	// with a max length route and a final hop that returned an intermediate
+	// payload. Blame the final hop.
+	if sender == 0 {
+		sender = o.hopCount
+		msg = nil
+	}
+
+	return &DecryptedAttrError{
+		DecryptedError: DecryptedError{
+			SenderIdx: sender,
+			Sender:    o.circuit.PaymentPath[sender-1],
+			Message:   msg,
+		},
+		Payloads: hopPayloads,
+	}, nil
+}
+
+func (o *OnionAttrErrorDecrypter) 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)
+}
+
+func (o *OnionAttrErrorDecrypter) shiftPayloadsLeft(payloads []byte) {
+	copy(payloads, payloads[o.payloadLen():o.hopCount*o.payloadLen()])
+}
+
+// extractPayload extracts the payload and payload origin information from the
+// given byte slice.
+func (o *OnionAttrErrorDecrypter) extractPayload(payloadBytes []byte) (
+	payloadSource, []byte, error) {
+
+	source := payloadSource(payloadBytes[0])
+
+	// Validate source indicator.
+	if source != payloadErrorNode && source != payloadIntermediateNode {
+		return 0, nil, errors.New("invalid payload source indicator")
+	}
+
+	// Extract payload.
+	payload := make([]byte, o.fixedPayloadLen)
+	copy(payload, payloadBytes[1:o.payloadLen()])
+
+	return source, payload, nil
+}
diff --git a/attributable_error_encrypt.go b/attributable_error_encrypt.go
new file mode 100644
index 0000000..8bece10
--- /dev/null
+++ b/attributable_error_encrypt.go
@@ -0,0 +1,171 @@
+package sphinx
+
+import (
+	"errors"
+	"fmt"
+)
+
+// ErrInvalidStructure is returned when the failure message has an invalid
+// structure. This is typically returned for messages that are shorter than the
+// minimum length.
+var ErrInvalidStructure = errors.New("failure message has invalid structure")
+
+// NewOnionAttrErrorEncrypter creates new instance of the onion
+// encrypter backed by the passed shared secret.
+func NewOnionAttrErrorEncrypter(sharedSecret Hash256,
+	structure *AttrErrorStructure) *OnionAttrErrorEncrypter {
+
+	return &OnionAttrErrorEncrypter{
+		AttrErrorStructure: *structure,
+
+		sharedSecret: sharedSecret,
+	}
+}
+
+// OnionAttrErrorEncrypter is a struct that's used to implement
+// attributable onion error encryption as defined within BOLT0004.
+type OnionAttrErrorEncrypter struct {
+	AttrErrorStructure
+
+	sharedSecret Hash256
+}
+
+func (o *OnionAttrErrorEncrypter) 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)
+	}
+}
+
+func (o *OnionAttrErrorEncrypter) shiftPayloadsRight(payloads []byte) {
+	copy(payloads[o.payloadLen():], payloads)
+}
+
+// addHmacs updates the failure data with a series of hmacs corresponding to all
+// possible positions in the path for the current node.
+func (o *OnionAttrErrorEncrypter) addHmacs(data []byte) {
+	message := o.message(data)
+	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)
+	}
+}
+
+// EncryptError is used to make data obfuscation using the generated shared
+// secret.
+//
+// In context of Lightning Network is either used by the nodes in order to make
+// initial obfuscation with the creation of the hmac or by the forwarding nodes
+// 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 *OnionAttrErrorEncrypter) EncryptError(initial bool,
+	data []byte, payload []byte) ([]byte, error) {
+
+	if len(payload) > o.fixedPayloadLen {
+		return nil, fmt.Errorf("payload exceeds maximum length")
+	}
+
+	if initial {
+		if len(data) < minPaddedOnionErrorLength {
+			return nil, fmt.Errorf(
+				"initial data size less than %v",
+				minPaddedOnionErrorLength,
+			)
+		}
+
+		data = o.initializePayload(data, payload)
+	} else {
+		if len(data) <
+			minPaddedOnionErrorLength+o.hmacsAndPayloadsLen() {
+
+			return nil, ErrInvalidStructure
+		}
+
+		o.addIntermediatePayload(data, payload)
+
+		// Shift hmacs to create space for the new hmacs.
+		o.shiftHmacsRight(o.hmacs(data))
+	}
+
+	// Update hmac block.
+	o.addHmacs(data)
+
+	// Obfuscate.
+	return onionEncrypt(&o.sharedSecret, data), nil
+}
+
+func (o *OnionAttrErrorEncrypter) initializePayload(message []byte,
+	payload []byte) []byte {
+
+	// Add space for payloads and hmacs.
+	data := make([]byte, len(message)+o.hmacsAndPayloadsLen())
+	copy(data, message)
+
+	payloads := o.payloads(data)
+
+	// Signal final hops in the payload.
+	addPayload(payloads, payloadErrorNode, payload)
+
+	return data
+}
+
+func (o *OnionAttrErrorEncrypter) addIntermediatePayload(data []byte,
+	payload []byte) {
+
+	payloads := o.payloads(data)
+
+	// Shift payloads to create space for the new payload.
+	o.shiftPayloadsRight(payloads)
+
+	// Signal intermediate hop in the payload.
+	addPayload(payloads, payloadIntermediateNode, payload)
+}
+
+func addPayload(payloads []byte, source payloadSource,
+	payload []byte) {
+
+	payloads[0] = byte(source)
+	copy(payloads[1:], payload)
+}
diff --git a/go.mod b/go.mod
index d274362..f049b44 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
 	github.com/davecgh/go-spew v1.1.1
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
+	github.com/stretchr/testify v1.8.1
 	golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
 )
 
diff --git a/go.sum b/go.sum
index fe83050..449e1a6 100644
--- a/go.sum
+++ b/go.sum
@@ -21,6 +21,7 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg
 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
 github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
 github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
@@ -53,6 +54,15 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
@@ -85,9 +95,13 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/testdata/attributable_error.json b/testdata/attributable_error.json
new file mode 100644
index 0000000..218f9e3
--- /dev/null
+++ b/testdata/attributable_error.json
@@ -0,0 +1,25 @@
+{
+    "encodedFailureMessage": "0140400f0000000000000064000c3500fd84d1fd012c80808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808002c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+    "hops": [
+        {
+            "sharedSecret": "b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328",
+            "encryptedMessage": "e88935bf7c9a374ba64fd9da3aa2a05e6cf9e52cfb1f72698fd33b74b9c79346284931ea3571af54de5df341304833b0825fb7e8817fd82a29c0803f0a0679a6a073c33a6fb8250090a3152eba3f11a85184fa87b67f1b0354d6f48e3b342e332a17b7710f342f342a87cf32eccdf0afc2160808d58abb5e5840d2c760c538e63a6f841970f97d2e6fe5b8739dc45e2f7f5f532f227bcc2988ab0f9cc6d3f12909cd5842c37bc8c7608475a5ebbe10626d5ecc1f3388ad5f645167b44a4d166f87863fe34918cea25c18059b4c4d9cb414b59f6bc50c1cea749c80c43e2344f5d23159122ed4ab9722503b212016470d9610b46c35dbeebaf2e342e09770b38392a803bc9d2e7c8d6d384ffcbeb74943fe3f64afb2a543a6683c7db3088441c531eeb4647518cb41992f8954f1269fb969630944928c2d2b45593731b5da0c4e70d0c84ad72f642fc26919927b347808bade4b1c321b08bc363f20745ba2f97f0ced2996a232f55ba28fe7dfa70a9ab0433a085388f25cce8d53de6a2fbd7546377d6ede9027ad173ba1f95767461a3689ef405ab608a21086165c64b02c1782b04a6dba2361a7784603069124e12f2f6dcb1ec7612a4fbf94c0e14631a2bef6190c3d5f35e0c4b32aa85201f449d830fd8f782ec758b0910428e3ec3ca1dba3b6c7d89f69e1ee1b9df3dfbbf6d361e1463886b38d52e8f43b73a3bd48c6f36f5897f514b93364a31d49d1d506340b1315883d425cb36f4ea553430d538fd6f3596d4afc518db2f317dd051abc0d4bfb0a7870c3db70f19fe78d6604bbf088fcb4613f54e67b038277fedcd9680eb97bdffc3be1ab2cbcbafd625b8a7ac34d8c190f98d3064ecd3b95b8895157c6a37f31ef4de094b2cb9dbf8ff1f419ba0ecacb1bb13df0253b826bec2ccca1e745dd3b3e7cc6277ce284d649e7b8285727735ff4ef6cca6c18e2714f4e2a1ac67b25213d3bb49763b3b94e7ebf72507b71fb2fe0329666477ee7cb7ebd6b88ad5add8b217188b1ca0fa13de1ec09cc674346875105be6e0e0d6c8928eb0df23c39a639e04e4aedf535c4e093f08b2c905a14f25c0c0fe47a5a1535ab9eae0d9d67bdd79de13a08d59ee05385c7ea4af1ad3248e61dd22f8990e9e99897d653dd7b1b1433a6d464ea9f74e377f2d8ce99ba7dbc753297644234d25ecb5bd528e2e2082824681299ac30c05354baaa9c3967d86d7c07736f87fc0f63e5036d47235d7ae12178ced3ae36ee5919c093a02579e4fc9edad2c446c656c790704bfc8e2c491a42500aa1d75c8d4921ce29b753f883e17c79b09ea324f1f32ddf1f3284cd70e847b09d90f6718c42e5c94484cc9cbb0df659d255630a3f5a27e7d5dd14fa6b974d1719aa98f01a20fb4b7b1c77b42d57fab3c724339d459ee4a1c6b5d3bd4e08624c786a257872acc9ad3ff62222f2265a658d9e2a007228a5293b67ec91c84c4b4407c228434bad8a815ca9b256c776bd2c9f92e2cf87a89ed786194104eba6b7973491f04d0b924c10e6fcff336d037b6e53fe763919da946a640960978994e8d0e5a2d555c9a897ce38a324c766fd01e9416acd91f1ea345c12aa1cfa5933c5c1230c5e45efb8c7e8d75bd9dd85ce8228cf80a52c915282375663690d1286ba0e70201af791a25715819dfd1035feb5239e3df7c230956cb3be858395094d3d99cbc2352cb8adc7a8fe8f4755a2f93bef3926f57bbcff17956c4031a2ae8c88d57dd9235b49a0253e86a7f173d96907aa2e162c82f1626dd5c07188e5e01d79724a546bed2b89a2084230b770ff97b2271158569ed7d00f967cdc51d216fa1578a9624f9142d8de1039b5d4f51de09324c91582f830c7730934feffbc7c51d5d87e8760a77e0712d947190ea6f896a4685045a3de3b8187490ee65f68a9c40cf708e03ab5f28a3b7e5e4a164c3cdb3a7a393b120a2306671a3e310419f873e1d978ff08535353a85eb1773e7476f1f102e3f2364036427a633d32cbf1c34ee0a223f696e69d9e296ac4981d64c99e9966d93eef673163ac774b2545e36e64816030b4ebb7775afeb77f88396c565d58bf2f2d07601dfc5338e5a5a71853dac2e42fc2d89a2da7bf913a8d5c1705ef3c869dc6a7d3a6a8ec7cde4e99380c0223b8d766506574f45cafbe96b25ebefd066945bffc1d2262d1e7d1057660ec2916055a493f2930afe8133de9e593b470e2d512ab5bedb363a6c9ac59fd82379f528d02bae1ade38fafe2d7ac7b097bb6fa4e00e43b087d3480f41402a5156379052526827da75486cf8703ed9ba34f38a9b445da35c3e5bd799156f25081e88051d54fe47fd0f7bca364d7cdeb01cb28aadc9b03fdd91712036f1da31a554e230a897e7142c0043c98e1d9a4bd996d26dfbce431bce8a6e29048783a7142c84483895c44df5d65c3bf8ca1bd6069ad305e525df7a4c584299525549bd3d013dcbfaebf18dcd82a9d29618a9b3e564b19cfdeac6eb7a6c4ad42268747fbd162c2f300aaad722d59dde7db179d93468b435724d97078df797d75d1728e75d0687e661fb603fa1b264466b8cedb75482ef042151f2ca045c795683a5e56dfff85a17f82cc5aba7e187784f159a996c3200b8b3f2a91de25d378557c5b33f2a17ec877b15cccc5615836899c30912cb83390e24902cbecb57cb95c0738d798debc43a07b09060347b145d5ccc150fcc46bc0a21f372622f25acae867946346e54498737be61c312e93086748b37633cbd8ab62433e6375914f117b9c1cb802513f"
+        },
+        {
+            "sharedSecret": "21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d",
+            "encryptedMessage": "89f5945b1ed1f4bbe9a33a706089c10f7c0dc4c0995fff7f4b5d9db50e04aca120031a33e1148091d8d3a3138f25397a6048d7f022a71f751e44a72d9b3f79809c8c51c9f0843daa8fe83587844fedeacb7348362003b31922cbb4d6169b2087b6f8d192d9cfe5363254cd1fde24641bde9e422f170c3eb146f194c48a459ae2889d706dc654235fa9dd20307ea54091d09970bf956c067a3bcc05af03c41e01af949a131533778bf6ee3b546caf2eabe9d53d0fb2e8cc952b7e0f5326a69ed2e58e088729a1d85971c6b2e129a5643f3ac43da031e655b27081f10543262cf9d72d6f64d5d96387ac0d43da3e3a03da0c309af121dcf3e99192efa754eab6960c256ffd4c546208e292e0ab9894e3605db098dc16b40f17c320aa4a0e42fc8b105c22f08c9bc6537182c24e32062c6cd6d7ec7062a0c2c2ecdae1588c82185cdc615a346e11eaf8f32cd44d5f1213d4738768f081978420697b454700ade1c093c02a6ca0e78a7e2f3d9e5c7e49e20c3a56b624bfea51196ec9e88e4e56be38ff56031369f45f1e03be826d44a182f270c153ee0d9f8cf9f1f4132f33974e37c7887d5b857365c873cb218cbf20d4be3abdb2a2011b14add0a5672e01e5845421cf6dd6faca1f2f443757aae575c53ab797c2227ecdab03882bbbf4599318cefafa72fa0c9a0f5a51d13c9d0e5d25bfcfb0154ed25895260a9df8743ac188714a3f16960e6e2ff663c08bffda41743d50960ea2f28cda0bc3bd4a180e297b5b41c700b674cb31d99c7f2a1445e121e772984abff2bbe3f42d757ceeda3d03fb1ffe710aecabda21d738b1f4620e757e57b123dbc3c4aa5d9617dfa72f4a12d788ca596af14bea583f502f16fdc13a5e739afb0715424af2767049f6b9aa107f69c5da0e85f6d8c5e46507e14616d5d0b797c3dea8b74a1b12d4e47ba7f57f09d515f6c7314543f78b5e85329d50c5f96ee2f55bbe0df742b4003b24ccbd4598a64413ee4807dc7f2a9c0b92424e4ae1b418a3cdf02ea4da5c3b12139348aa7022cc8272a3a1714ee3e4ae111cffd1bdfd62c503c80bdf27b2feaea0d5ab8fe00f9cec66e570b00fd24b4a2ed9a5f6384f148a4d6325110a41ca5659ebc5b98721d298a52819b6fb150f273383f1c5754d320be428941922da790e17f482989c365c078f7f3ae100965e1b38c052041165295157e1a7c5b7a57671b842d4d85a7d971323ad1f45e17a16c4656d889fc75c12fc3d8033f598306196e29571e414281c5da19c12605f48347ad5b4648e371757cbe1c40adb93052af1d6110cfbf611af5c8fc682b7e2ade3bfca8b5c7717d19fc9f97964ba6025aebbc91a6671e259949dcf40984342118de1f6b514a7786bd4f6598ffbe1604cef476b2a4cb1343db608aca09d1d38fc23e98ee9c65e7f6023a8d1e61fd4f34f753454bd8e858c8ad6be649cc7c5ebe91be307bcd3ef972eac04ee1411897667db31217e01aad868554ab56be9a21a0827e29cd8829428ddbe7bb86f23d0a46aa6d54aa36e9b61498e690229236bd5e7b25afbc5a31661cd9713b904e57c17187a147f2eafedd595db46b26e5a9f6393b82a55e81a93d72f4b050fbedcd3a527992e8b4f2f5a1ca36aff1037e645937f2633c27a3cd0902342527df7617d332efb2199718fc5646aaba27826267e79705f766e8a0d6f2249dad7f5814957c48d1ef27f94317ad4e523289315b34b4f47579814e6513bc55ea6535fc6963388dbcbe6caa8d708f1f800cdf4c6060bfb69e001d8664a02fb09698b4f1b8fc4a2787ef9080d22b0b4f19dafce139769bcf532ad1e21d5cb7a7e65061444030ca30726a69f09756c8e2e6a37feea37af317ff77f6303922472ca6b261de9ae33626c9880df0b48667dbba7b04b7e7836519dab7358567c7324d106c831657528d86ec2edfd845faae8ea94223254fbefb738db202e0eca9001b3313127d1a33f470c4b29b05280b3b2f7b4bfc9f33497ffef59e78a120513e63d743d60c1ff176f40283de63ce4d37d9cc99af46ed15dd58e61dc1669d8bfa5b0eda8feda7dc511b1d07c26452dacefb5ad5377669785ade7d3a8c53dedfacbfa4461416c7cb4a5dc9140194088d2672f4c0f7caa214d2d1705e4d73bc3632639b5ef15b49bd7c50de3f2ab66608c25d212711a23c6a0ef9f22b224fa1a2059d8174aad941094cfd05cc6cd5f634ec5d0f4cec8454d1ee4817c48b36d2089d8c5c0e970e9839ae33b10bd0969e8af37e38f4361de930546d07f3e3745dac990fe470fda1d3b1041d3eaad6d19ef6e70c1585ef9f3d14280a4d3730104dfa49d3fe40e51e7c49eef6db35011c7e7f4daca2f0f36fc87742306208f10e6b85415f53f3a2316a2f124136aa6b6a9a2a5a43bc755f481344d3067bd28810adfe60cba3dec1e11cb66c4648749b42108cab1dc85fdb5bf28d4c7c95e273cd8203a35831b35ce40f4153778f0ddd200ea2652032840a3ddb1e46a2ab383d18ef9b9db426d016c6bfb9a05068f04f2a54be9405cac27c2e601568bdbaafeb5353363b2ab07168758a94ae6bc903e57e0cd41a2c753d2ccc5b6988a77637efc22ed648e2fb6eeac5504dfc05553f3f611f4f56248f170eabf4ddb19b4d006cbf6358f384914f1bd586c48b63aea04e35bea4fa5e56d8ed6780f30aaee19171f804532d654663c66ff4593ce7b495525c66a10bb7935f068a0154ae2a13704ab80176d1283d3e92bbf663ee100425ac01c73137e36ab8d8c845a725c3ecabc6973d35dfd"
+        },
+        {
+            "sharedSecret": "3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc",
+            "encryptedMessage": "e8bc60d044af7b8686febd7b1ae04f2f30fb809233ebb730ba837bc4f06751ea7f1862ddc1535f37c6eef4789bbed4d5e34d5875d2cd2e07bd669dfb5f4c54162fa504138dabd6ebcf0db8017840c35f12a2cfb84f89cc7c8959a6d51815b1d2c5136cedec2e4106bb5f2af9a21bd0a02c40b44ded6e6a90a145850614fb1b0eef2a03389f3f2693bc8a755630fc81fff1d87a147052863a71ad5aebe8770537f333e07d841761ec448257f948540d8f26b1d5b66f86e073746106dfdbb86ac9475acf59d95ece037fba360670d924dce53aaa74262711e62a8fc9eb70cd8618fbedae22853d3053c7f10b1a6f75369d7f73c419baa7dbf9f1fc5895362dcc8b6bd60cca4943ef7143956c91992119bccbe1666a20b7de8a2ff30a46112b53a6bb79b763903ecbd1f1f74952fb1d8eb0950c504df31fe702679c23b463f82a921a2c1155802b8866064f7bad07a50da5cf31f8c3151c4c52e525fb22ecf48f8fa39bb5adf932b50c12c10be90174b37d454a3f8b284c849e86578a6182c4a7b2e47dd57d44730a1be9fec4ad07287a397e28dce4fda57e9cdfdb2eb5afdf0d38ef19d982341d18d07a556bb16c1416f480a396f278373b8fd9897023a4ac506e65cf4c306377730f9c8ca63cf47565240b59c4861e52f1dab84d938e96fb31820064d534aca05fd3d2600834fe4caea98f2a748eb8f200af77bd9fbf46141952b9ddda66ef0ebea17ea1e7bb5bce65b6e71554c56dd0d4e14f4cf74c77a150776bf31e7419756c71e7421dc22efe9cf01de9e19fc8808d5b525431b944400db121a77994518d6025711cb25a18774068bba7faaa16d8f65c91bec8768848333156dcb4a08dfbbd9fef392da3e4de13d4d74e83a7d6e46cfe530ee7a6f711e2caf8ad5461ba8177b2ef0a518baf9058ff9156e6aa7b08d938bd8d1485a787809d7b4c8aed97be880708470cd2b2cdf8e2f13428cc4b04ef1f2acbc9562f3693b948d0aa94b0e6113cafa684f8e4a67dc431dfb835726874bef1de36f273f52ee694ec46b0700f77f8538067642a552968e866a72a3f2031ad116663ac17b172b446c5bc705b84777363a9a3fdc6443c07b2f4ef58858122168d4ebbaee920cefc312e1cea870ed6e15eec046ab2073bbf08b0a3366f55cfc6ad4681a12ab0946534e7b6f90ea8992d530ec3daa6b523b3cf03101c60cadd914f30dec932c1ef4341b5a8efac3c921e203574cfe0f1f83433fddb8ccfd273f7c3cab7bc27efe3bb61fdccd5146f1185364b9b621e7fb2b74b51f5ee6be72ab6ff46a6359dc2c855e61469724c1dbeb273df9d2e1c1fb74891239c0019dc12d5c7535f7238f963b761d7102b585372cf021b64c4fc85bfb3161e59d2e298bba44cfd34d6859d9dba9dc6271e5047d525468c814f2ae438474b0a977273036da1a2292f88fcfb89574a6bdca11bb685616be825941bd8b2cac7d37d1a28e5a4531601bfa9146c590e2bc0ce1fdabd288184b1dea153ad23f85402dc956e0b25f025dd2456f74b0d44b0d8e2d244fcaa04c65f1d027519158a8e5a83b00988d8c8bded5a8478326738e2b9ab467f27c42fecec7f6e48f5ee435266220b6c6ebfd9eb7a50b96d665f018adb86427a46208f39e63ab71876131d50bdb524367f5d538cfdba78a94cd1ca4e6381749f8823a100ea020169f16596f3d8cfff49dbda803c3afb89c7fcfec9dbe979b89882a347fa8e441b1d3bea00654e247912ccc4b85946c38f6a7c32d6b102c596dfa9bd78a24603d8ae93a713c342660036249d60fd0c3874c2e9545b3d099afaffe9dfced8e9123f4720de054e6d91329abcd91b07907af03673652823f9988039f87ebfa2ff156831c8881d16776771ec81b20ee37918b5fe81bef9e6710d4430c8e924ca15e8b5d061982e369923f819b1313ad94b9bc0e846df659367079f0113bb43f7c99dc5d5ae8958c605d7bafaed7c04a078c16d1cc8bd622a92676269452f06dcdc3a0891a091ed9403606d482d4cd7e99b067bb66ff3cf428a2fc4469bafc76ae172402338e3a59a3a607361293192385c141e31caa6a49615115ffaca858b400385be7e5472e35afb2a5b4b761b8c69767727516f974734f5d00b917deb2ec7a5ca05fb3683fc0faf155bec35854359707da65fa13df32ba237ef0ae21da9f60e68f157a196b1155e5fb33f43789ba9f43d2f6852c63c147f26a51dc5fc767326e2b56464efe376afb90c4a90d071d288051fd8af10ffb52421bbbdbe37f3d83dd6408f33241f54b135460ac285df9b27cd6ab2c109996f3fb4eb6244f9daa7920bb492c0958caa0f4239205e50c7297b575dbb9ce8f3154a5a5a9afa7eb80dd4b9de6143603b135fe4e0471a49fb8b364e0f828a32f57a21dec19a41d999e2f9b8c262437016d9d5cd62517a1bed4a8fb1d9d71e6058b2b22386e4cb5699ad04ab358a49359376cb9e360645758fcd28031705b48f393ca7d4cead494939e9a6956eaa5af0666897f0ce525dc6fff80b1c2b972ccc5d9a0b4bd710396c416991d6bbc545b2e0348c249ed2c3f3032e350252e05c974ee73d72341b466e40d0e89e132f17759ab9d0764e1c1e2f3464aa22cb928a9adc4216dd31a39e59ddc9d8b775d946bde0f651b2d5a32e6da41ffddbe7520a75329a5ac1c2e75f2a73d7b67ca41923b6517b7101b46304604607348ae458ca70de9f4fda848f9076145a0a6d8f26a1c6504a018c3f79f9d3a74c37a7ac5785acd87bf7ac39c148135f8d956b20eb396abe"
+        },
+        {
+            "sharedSecret": "a6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae",
+            "encryptedMessage": "e7aca8bf4139f65f977604e3f4a593f2cb031f7b9a711107e6c19691d0c5060a375d0d39d6920d2e08d72d59cee7827bccbacd62aeaee61e9e59a630c4c77cf383cb37b07413aa4de2f2fbf5b40ae40a91a8f4c6d74aeacef1bb1be4ecbc26ec2c824d2bc45db4b9098e732a769788f1cff3f5b41b0d25c132d40dc5ad045ef0043b15332ca3c5a09de2cdb17455a0f82a8f20da08346282823dab062cdbd2111e238528141d69de13de6d83994fbc711e3e269df63a12d3a4177c5c149150eb4dc2f589cd8acabcddba14dec3b0dada12d663b36176cd3c257c5460bab93981ad99f58660efa9b31d7e63b39915329695b3fa60e0a3bdb93e7e29a54ca6a8f360d3848866198f9c3da3ba958e7730847fe1e6478ce8597848d3412b4ae48b06e05ba9a104e648f6eaf183226b5f63ed2e68f77f7e38711b393766a6fab7921b03eb2a6bddfc370eb45c1699c856969e2d574fdd155945ed727fdf2aec4f056a4d49fdefc3abafe41c365a5bd14fd486d6b5e2f24199319e7813e02e798877ffe31a70ae2398d9e31b9e3727e6c1a3c0d995c67d37bb6e72e9660aaaa9232670f382add2edd468927e3303b6142672546997fe105583e7c5a3c4c2b599731308b5416e6c9a3f3ba55b181ad0439d3535356108b059f2cb8742eed7a58d4eba9fe79eaa77c34b12aff1abdaea93197aabd0e74cb271269ca464b3b06aef1d6573df5e1224179616036b368677f26479376681b772d3760e871d99efd34cca5cd6beca95190d967da820b21e5bec60082ea46d776b0517488c84f26d12873912d1f68fafd67bcf4c298e43cfa754959780682a2db0f75f95f0598c0d04fd014c50e4beb86a9e37d95f2bba7e5065ae052dc306555bca203d104c44a538b438c9762de299e1c4ad30d5b4a6460a76484661fc907682af202cd69b9a4473813b2fdc1142f1403a49b7e69a650b7cde9ff133997dcc6d43f049ecac5fce097a21e2bce49c810346426585e3a5a18569b4cddd5ff6bdec66d0b69fcbc5ab3b137b34cc8aefb8b850a764df0e685c81c326611d901c392a519866e132bbb73234f6a358ba284fbafb21aa3605cacbaf9d0c901390a98b7a7dac9d4f0b405f7291c88b2ff45874241c90ac6c5fc895a440453c344d3a365cb929f9c91b9e39cb98b142444aae03a6ae8284c77eb04b0a163813d4c21883df3c0f398f47bf127b5525f222107a2d8fe55289f0cfd3f4bbad6c5387b0594ef8a966afc9e804ccaf75fe39f35c6446f7ee076d433f2f8a44dba1515acc78e589fa8c71b0a006fe14feebd51d0e0aa4e51110d16759eee86192eee90b34432130f387e0ccd2ee71023f1f641cddb571c690107e08f592039fe36d81336a421e89378f351e633932a2f5f697d25b620ffb8e84bb6478e9bd229bf3b164b48d754ae97bd23f319e3c56b3bcdaaeb3bd7fc0369b609e1d6b7f9a98d6f139d335d34022afc76f3c22354b0b43dd865b1c211cf8b1850ac389a1ba507ecc961434b3b61e906895d17f34580cf3ceefd4d77c06c1560aea2b20798f3217c3fab544800b1b19538971f4650217c566f14bb6845e210a0339bed7b93a9d53ae8a6bdca8ede02544601bb6da225d0b138e4217c33c1be2b964a265a8065b893dbcc8fcf74a861311136c5f236b7c3cd2d28e5175789bcf6cbc9974b6ce6285fd958cb7f6f5e5d879a76b4fd6619c34d643120383791a2ce6bfc611885319a13f47c7ba2e8c1ac4c50da3714b5a3c63b0afaa73cbbebc3724383da6c044ff5480fc3ec4d8b43b45dbe206de176e443fcdd383fbacdf3e4256a13068b9750dd1ff605a7f340edd9dbad0f213d828d5ad10bc99189b8b3b3bfeea855c02050e51f66c402b588b9b7c354875b259e128702f6760cd6f77e9dfed0db82075aadb3ccbd5e7024d819e11f1fe57fee830d3b732a9d409d3ee39824857843b41c9d17b6e78bdd014209a2e9006a2823940bd7750ee94a1011e55428933ed90a588feefdb1bbb684ad5538eb4126488812dda96d8b68de37302458d2c06121877a33d2a0decf879407cc53f6a02b5b111b2a4504ac10a8c770e039a68cf50c5ba146dea36bbc8cef7cdcce348fbc607dca64214d9045adbc8987a8ab76666c6f4e219b2e53a53d46740153b25e3e872037ff0322f03159ce4309511d97b8b9d5ba43c8d49ef7962ceacbf04d5c704deb341917ca18fbf3ff6025ffb3b2b400a3ca6fa32a3672d29c3530eb68a5425f022c9958993f92ebcee24a64046ff1dde5ae330016eef4536e2e0a13aa2e13ff3c93fa241c5bcba94bd9493fe8b453b24497a41c832f2899b52b5eaba962c8456fac88b4d6857b2f6d45330d2447142c447038b46177ee57693b9387f5b2ac7a491df3e03094b36b6ccdf668abe836571850476540ac313269bdbed5417f0f6f56f1a35e0a37a28c85f6268b6a89e29ddbd45593f6d69e9ea550c41fc9d265a2a552e1423cc1005487ba118d1164871a16ebe3933b05fb3a3c8614adbf0a4513eaf5720b81794ecce8d9048c8856379760c7e382f8fb0ad7978070087fa1f8c064fa671269bb64c4b1fc9f860b84755a6fda1ef798c1033a1e7a4719c78201acadb2165bc97026f66a21b556d4d09b1e8b0e29e050f6f034a9e649a4a2d1d34f42263b24c4e257909ab62d259188aab42f77f1018490d1de38ef9af5b973335537b09e3816b117a8bb187cb7bd30a1779aec88cf70821a6a0ee4d76e220dbdbacb37e777dd863eccbfc86c04650916c0adbc40d3b7f80"
+        },
+        {
+            "sharedSecret": "53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66",
+            "encryptedMessage": "d135558a6bad7c5a617272dfff6fba9a1ee9d5509b697129b42407b020959566ba84011d09efc5e187c148ba548bf7922e7a5d6c0ceb2ff3a559ae244acd9d783c65977765c5d4e00b723d00f12475aafaafff7b31c1be5a589e6e25f8da2959107206dd42bbcb43438129ce6cce2b6b4ae63edc76b876136ca5ea6cd1c6a04ca86eca143d15e53ccdc9e23953e49dc2f87bb11e5238cd6536e57387225b8fff3bf5f3e686fd08458ffe0211b87d64770db9353500af9b122828a006da754cf979738b4374e146ea79dd93656170b89c98c5f2299d6e9c0410c826c721950c780486cd6d5b7130380d7eaff994a8503a8fef3270ce94889fe996da66ed121741987010f785494415ca991b2e8b39ef2df6bde98efd2aec7d251b2772485194c8368451ad49c2354f9d30d95367bde316fec6cbdddc7dc0d25e99d3075e13d3de0822e4d8e15a7c521d67ce2cc836c49118f205c99f18570504504221e337a29e2716fb28671b2bb91e38ef5e18aaf32c6c02f2fb690358872a1ed28166172631a82c2568d23238017188ebbd48944a147f6cdb3690d5f88e51371cb70adf1fa02afe4ed8b581afc8bcc5104922843a55d52acde09bc9d2b71a663e178788280f3c3eae127d21b0b95777976b3eb17be40a702c244d0e5f833ff49dae6403ff44b131e66df8b88e33ab0a58e379f2c34bf5113c66b9ea8241fc7aa2b1fa53cf4ed3cdd91d407730c66fb039ef3a36d4050dde37d34e80bcfe02a48a6b14ae28227b1627b5ad07608a7763a531f2ffc96dff850e8c583461831b19feffc783bc1beab6301f647e9617d14c92c4b1d63f5147ccda56a35df8ca4806b8884c4aa3c3cc6a174fdc2232404822569c01aba686c1df5eecc059ba97e9688c8b16b70f0d24eacfdba15db1c71f72af1b2af85bd168f0b0800483f115eeccd9b02adf03bdd4a88eab03e43ce342877af2b61f9d3d85497cd1c6b96674f3d4f07f635bb26add1e36835e321d70263b1c04234e222124dad30ffb9f2a138e3ef453442df1af7e566890aedee568093aa922dd62db188aa8361c55503f8e2c2e6ba93de744b55c15260f15ec8e69bb01048ca1fa7bbbd26975bde80930a5b95054688a0ea73af0353cc84b997626a987cc06a517e18f91e02908829d4f4efc011b9867bd9bfe04c5f94e4b9261d30cc39982eb7b250f12aee2a4cce0484ff34eebba89bc6e35bd48d3968e4ca2d77527212017e202141900152f2fd8af0ac3aa456aae13276a13b9b9492a9a636e18244654b3245f07b20eb76b8e1cea8c55e5427f08a63a16b0a633af67c8e48ef8e53519041c9138176eb14b8782c6c2ee76146b8490b97978ee73cd0104e12f483be5a4af414404618e9f6633c55dda6f22252cb793d3d16fae4f0e1431434e7acc8fa2c009d4f6e345ade172313d558a4e61b4377e31b8ed4e28f7e3d387988960f41b82bdda83a4e39828a8644af0377cb3f1cfa6bb26cfa878a5d4599ad2cbd72475931259903165cccb17679e3d0aaa2577f798de15b227f4b0dbf92b8ef891d17c814ab2e4e599b01f4b66c830b7dbbcfb3ab33a9e0af7398c4076f2256986710f7ca41cd8b477579a2e45c32830a3498baf7607b2ead37b75242a7c58dfe486b9b959ff639d04c2b0398fd00a8f8ee76f531b72fd5a3f09ed79e0de573c228f32488179c302be2677576354e22079eeb0a3ffaef65c875078c714d16ed36cf10892352b76d14764f51f0ffbb62f98faf263f41e84ec1ab0d4f723d279879f655be2c5caef768ed6be313a751f0f2af9e3517107f23012c54f36bdcc96f0f1c721bcb8a927cf2ed82e886d0a83975d00e9a0ff849cb9b44d8c030c293c9a02a47df169f4684a84b03c843ac2955bd7566909bb93ef5ae14b3be4fb481358b0e0894a8b4ff6599e4fac7bfb48841aa660dd1b70193e492221ec5d4a28f4dad0818ed1d72ef08da12eb7583f29276586a3e69f10bbe13ba4afca584e86ce2ce599cc7df8b98318a8d448fb3c75ad94671f9d80bc7a1660dc716422176fb19fa1e959d204116d538d2b0ca478b2c9f0ade86cf515d54e95676b0b17a508c13f97deb70bdbf4bc45f4ead427951d7455f892ea20d6a9bac83446a2217607747287b2a990bc8a7a823884d94fbcca38aceb666ca2afc47e8b9d8a3eeea093c92876b83d4e41e032d30365eea2cd717dc70f79cc4c9b7695e81769d2a0c30ca02b98f834bfb968948f150670d1cec91500c27301fc0d01faeacaa96fe0800b177bae2b57eee7909000934b597eb8517ddb11136f5235ce8d7204c22a6784fe8c187121457715b7383ee699a42df1d58a413725c86730d704538af546e7d1a70a15a968f5e7aa9afedcaf8a8e4909d7af6840d9018f8c3ba42f697eca710d31344b0ec20c8e9020e271e67c3822021afdbbbe2b77ef8e2e5d1b6a47865bf5cf925758df8e3a68e0be4cd0ece8c1bf26bf41686a5632bf9f9c720999b62d7d0a6d69bc3afe3d3f202a51ab5e077f76f4f77b9533de8151fbc431e453ad6c1bd6dd65a5c034d9cedcbaef57f4bbeff247ce4d0d5bfba92442bbd34e823edc351080732df64abdac453d982d8cb305dd583b7ffb7296d9d6076c411c3a36f76f5965a78d01795000fa1311cf08a662c74a9aa5369fadd6c8cf29d3de62303b51473b403ffe12a7164b6dc756b3d57377d0528a793b4c478c15a1cb68fc13318182095cc1a756db47ff3e141e59d783a2a87a0c3822f6644cb96813645f62ea76878be6d2ae36ef14f80e"
+        }
+    ]
+}
\ No newline at end of file