Skip to content
99 changes: 72 additions & 27 deletions transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"errors"
"fmt"
"sort"
"strings"

"github.com/onflow/go-ethereum/rlp"

Expand Down Expand Up @@ -102,7 +103,7 @@ type payloadCanonicalForm struct {

type envelopeCanonicalForm struct {
Payload payloadCanonicalForm
PayloadSignatures []transactionSignatureCanonicalForm
PayloadSignatures []interface{}
Comment thread
tarakby marked this conversation as resolved.
Outdated
}

type transactionCanonicalForm struct {
Expand Down Expand Up @@ -335,7 +336,8 @@ func (t *Transaction) SignEnvelope(address Address, keyIndex uint32, signer cryp

// AddPayloadSignature adds a payload signature to the transaction for the given address and key index.
func (t *Transaction) AddPayloadSignature(address Address, keyIndex uint32, sig []byte) *Transaction {
Comment thread
jribbink marked this conversation as resolved.
s := t.createSignature(address, keyIndex, sig)
// to properly support extension data, the parent function must pass in the extension data
s := t.createSignature(address, keyIndex, sig, nil)

t.PayloadSignatures = append(t.PayloadSignatures, s)
sort.Slice(t.PayloadSignatures, compareSignatures(t.PayloadSignatures))
Expand All @@ -345,25 +347,26 @@ func (t *Transaction) AddPayloadSignature(address Address, keyIndex uint32, sig

// AddEnvelopeSignature adds an envelope signature to the transaction for the given address and key index.
func (t *Transaction) AddEnvelopeSignature(address Address, keyIndex uint32, sig []byte) *Transaction {
s := t.createSignature(address, keyIndex, sig)
s := t.createSignature(address, keyIndex, sig, nil)

t.EnvelopeSignatures = append(t.EnvelopeSignatures, s)
sort.Slice(t.EnvelopeSignatures, compareSignatures(t.EnvelopeSignatures))
t.refreshSignerIndex()
return t
}

func (t *Transaction) createSignature(address Address, keyIndex uint32, sig []byte) TransactionSignature {
func (t *Transaction) createSignature(address Address, keyIndex uint32, sig []byte, extensionData []byte) TransactionSignature {
signerIndex, signerExists := t.signerMap()[address]
if !signerExists {
signerIndex = -1
}

return TransactionSignature{
Address: address,
SignerIndex: signerIndex,
KeyIndex: keyIndex,
Signature: sig,
Address: address,
SignerIndex: signerIndex,
KeyIndex: keyIndex,
Signature: sig,
ExtensionData: extensionData,
}
}

Expand Down Expand Up @@ -434,7 +437,18 @@ func (t *Transaction) Encode() []byte {
func DecodeTransaction(transactionMessage []byte) (*Transaction, error) {
temp, err := decodeTransaction(transactionMessage)
if err != nil {
return nil, err
// If the transaction is in the legacy format, convert it to the canonical form
if strings.Contains(err.Error(), "too few elements") { // since the rlp library does not have this error type, just check string
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using string matching for error handling is fragile. Consider defining a custom error type or using error wrapping/unwrapping patterns for more robust error handling.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better check than this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really, this is working off the go ethereum error: https://github.com/ethereum/go-ethereum/blob/master/rlp/decode.go#L108-L112

which is

type decodeError struct {
	msg string
	typ reflect.Type
	ctx []string
}

so only really gives you msg to work with. There is no additional ctx for the error we're working with.

// try legacy decoding
legacyTemp, err := decodeTransactionLegacy(transactionMessage)
if err != nil {
return nil, err
}
// convert legacy to canonical form
temp = legacyTemp
} else {
return nil, err
}
}

authorizers := make([]Address, len(temp.Payload.Authorizers))
Expand Down Expand Up @@ -531,6 +545,7 @@ func decodeTransaction(transactionMessage []byte) (*transactionCanonicalForm, er

// Decode the payload sigs
payloadSigs := []transactionSignatureCanonicalForm{}
fmt.Println(s.Kind())
Comment thread
tarakby marked this conversation as resolved.
Outdated
err = s.Decode(&payloadSigs)
if err != nil {
return nil, err
Expand Down Expand Up @@ -564,31 +579,61 @@ type ProposalKey struct {

// A TransactionSignature is a signature associated with a specific account key.
type TransactionSignature struct {
Address Address
SignerIndex int
KeyIndex uint32
Signature []byte
Address Address
SignerIndex int
KeyIndex uint32
Signature []byte
ExtensionData []byte
}

type transactionSignatureCanonicalForm struct {
SignerIndex uint
KeyIndex uint32
Signature []byte
}

func (s TransactionSignature) canonicalForm() transactionSignatureCanonicalForm {
SignerIndex uint
KeyIndex uint32
Signature []byte
ExtensionData []byte
}

// Checks if the scheme is plain authentication scheme, and indicate that it
// is required to use the legacy canonical form.
// We check for a valid scheme identifier, as this should be the only case
// where the extension data can be left out of the cannonical form.
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: 'cannonical' should be 'canonical'.

Copilot uses AI. Check for mistakes.
// All other non-valid cases that are similar to the plain scheme, but is not valid,
// should be included in the canonical form, as they are not valid signatures
func (s TransactionSignature) shouldUseLegacyCanonicalForm() bool {
plainSchemeIdentifier := byte(0)
Comment thread
Kay-Zee marked this conversation as resolved.
Outdated
// len check covers nil case
return len(s.ExtensionData) == 0 || (len(s.ExtensionData) == 1 && s.ExtensionData[0] == plainSchemeIdentifier)
}

func (s TransactionSignature) canonicalForm() interface{} {
// Until we deprecate the old TransactionSignature format, we need to have two canonical forms.
// int is not RLP-serializable, therefore s.SignerIndex and s.KeyIndex are converted to uint
if s.shouldUseLegacyCanonicalForm() {
// This is the legacy cononical form, mainly here for backward compatibility
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: 'cononical' should be 'canonical'.

Copilot uses AI. Check for mistakes.
return struct {
Comment thread
tarakby marked this conversation as resolved.
Outdated
SignerIndex uint
KeyIndex uint32
Signature []byte
}{
SignerIndex: uint(s.SignerIndex),
KeyIndex: s.KeyIndex,
Signature: s.Signature,
}
}
return transactionSignatureCanonicalForm{
SignerIndex: uint(s.SignerIndex), // int is not RLP-serializable
KeyIndex: s.KeyIndex, // int is not RLP-serializable
Signature: s.Signature,
SignerIndex: uint(s.SignerIndex), // int is not RLP-serializable
KeyIndex: s.KeyIndex, // int is not RLP-serializable
Signature: s.Signature,
ExtensionData: s.ExtensionData,
}
}

func transactionSignatureFromCanonicalForm(v transactionSignatureCanonicalForm) TransactionSignature {
return TransactionSignature{
SignerIndex: int(v.SignerIndex),
KeyIndex: v.KeyIndex,
Signature: v.Signature,
SignerIndex: int(v.SignerIndex),
KeyIndex: v.KeyIndex,
Signature: v.Signature,
ExtensionData: v.ExtensionData,
}
}

Expand All @@ -607,8 +652,8 @@ func compareSignatures(signatures []TransactionSignature) func(i, j int) bool {

type signaturesList []TransactionSignature

func (s signaturesList) canonicalForm() []transactionSignatureCanonicalForm {
signatures := make([]transactionSignatureCanonicalForm, len(s))
func (s signaturesList) canonicalForm() []interface{} {
signatures := make([]interface{}, len(s))

for i, signature := range s {
signatures[i] = signature.canonicalForm()
Expand Down
139 changes: 139 additions & 0 deletions transaction_legacy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Flow Go SDK
*
* Copyright Flow Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package flow

import (
"bytes"
"errors"
"fmt"

"github.com/onflow/go-ethereum/rlp"
Comment thread
Kay-Zee marked this conversation as resolved.
Outdated
)

type transactionLegacyCanonicalForm struct {
Payload payloadCanonicalForm
PayloadSignatures []transactionSignatureLegacyCanonicalForm
EnvelopeSignatures []transactionSignatureLegacyCanonicalForm
}

type transactionSignatureLegacyCanonicalForm struct {
SignerIndex uint
KeyIndex uint32
Signature []byte
}

func (s *transactionLegacyCanonicalForm) convertToCanonicalForm() *transactionCanonicalForm {
canonicalPayloadSigs := make([]transactionSignatureCanonicalForm, 0, len(s.PayloadSignatures))
for _, sig := range s.PayloadSignatures {
canonicalPayloadSigs = append(canonicalPayloadSigs, transactionSignatureCanonicalForm{
SignerIndex: sig.SignerIndex,
KeyIndex: sig.KeyIndex,
Signature: sig.Signature,
})
}

canonicalEnvelopSigs := make([]transactionSignatureCanonicalForm, 0, len(s.EnvelopeSignatures))
for _, sig := range s.EnvelopeSignatures {
canonicalEnvelopSigs = append(canonicalEnvelopSigs, transactionSignatureCanonicalForm{
SignerIndex: sig.SignerIndex,
KeyIndex: sig.KeyIndex,
Signature: sig.Signature,
})
}

return &transactionCanonicalForm{
Payload: s.Payload,
PayloadSignatures: canonicalPayloadSigs,
EnvelopeSignatures: canonicalEnvelopSigs,
}
}

func decodeTransactionLegacy(transactionMessage []byte) (*transactionCanonicalForm, error) {
s := rlp.NewStream(bytes.NewReader(transactionMessage), 0)
temp := &transactionLegacyCanonicalForm{}

kind, _, err := s.Kind()
if err != nil {
return nil, err
}

// First kind should always be a list
if kind != rlp.List {
return nil, errors.New("unexpected rlp decoding type")
}

_, err = s.List()
if err != nil {
return nil, err
}

// Need to look at the type of the first element to determine if how we're going to be decoding
kind, _, err = s.Kind()
if err != nil {
return nil, err
}
// If first kind is not list, safe to assume this is actually just encoded payload, and decrypt as such
if kind != rlp.List {
s.Reset(bytes.NewReader(transactionMessage), 0)
txPayload := payloadCanonicalForm{}
err := s.Decode(&txPayload)
if err != nil {
return nil, err
}
temp.Payload = txPayload
return temp.convertToCanonicalForm(), nil
}

// If we're here, we will assume that we're decoding either a envelopeCanonicalForm
// or a full transactionCanonicalForm

// Decode the payload
txPayload := payloadCanonicalForm{}
err = s.Decode(&txPayload)
if err != nil {
return nil, err
}
temp.Payload = txPayload

// Decode the payload sigs
payloadSigs := []transactionSignatureLegacyCanonicalForm{}
fmt.Println(s.Kind())
Comment thread
tarakby marked this conversation as resolved.
Outdated
err = s.Decode(&payloadSigs)
if err != nil {
return nil, err
}
temp.PayloadSignatures = payloadSigs

// It's possible for the envelope signature to not exist (e.g. envelopeCanonicalForm).
kind, _, err = s.Kind()
if errors.Is(err, rlp.EOL) {
return temp.convertToCanonicalForm(), nil
} else if err != nil {
return nil, err
}
// If we're not at EOL, and no error, finish decoding
envelopeSigs := []transactionSignatureLegacyCanonicalForm{}
err = s.Decode(&envelopeSigs)
if err != nil {
return nil, err
}
temp.EnvelopeSignatures = envelopeSigs

return temp.convertToCanonicalForm(), nil
}
2 changes: 1 addition & 1 deletion transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func ExampleTransaction() {
// Envelope signatures:
// Address: ee82856bf20e2aa6, Key Index: 7, Signature: 03
//
// Transaction ID (after signing): d1a2c58aebfce1050a32edf3568ec3b69cb8637ae090b5f7444ca6b2a8de8f8b
// Transaction ID (after signing): d5d0f64f23650567657a7b2073f81df58e1eff3eb1c7af69eb83d65c4e25f4e5
Comment thread
tarakby marked this conversation as resolved.
Outdated
}

func TestTransaction_SetScript(t *testing.T) {
Expand Down
Loading