From fa9d6e0252a8c2457ce675d1f5982c38048b1a18 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Fri, 31 Jul 2026 09:21:03 +1200 Subject: [PATCH 1/4] fix: cancel signing only when collected signatures meet the threshold BuildRegularSignature and BuildNoChainIDSignature cancelled outstanding signers using signersWeight, which counts payload-blind subdigest leaves as satisfied for any payload. A config containing such a leaf therefore looked satisfied as soon as the first signature arrived, and cancellation nondeterministically dropped signatures that recovery of a non-matching payload still needs. Add collectedSignersWeight, which counts subdigest leaves as zero, and use it for the cancellation decision. validateSigningPower still checks the optimistic signersWeight, so validation semantics are unchanged. This makes the orchestrated path safe for pre-collected signatures, so BuildIntentConfigurationSignature returns to BuildRegularSignature and the BuildRegularSignatureFromSignatures workaround is removed. Co-Authored-By: Claude Fable 5 --- core/v3/signature_build_test.go | 68 ++++++++++++++++++++++++++++ core/v3/v3.go | 80 ++++++++++++++++++++------------- intent_config.go | 22 +++++---- 3 files changed, 130 insertions(+), 40 deletions(-) create mode 100644 core/v3/signature_build_test.go diff --git a/core/v3/signature_build_test.go b/core/v3/signature_build_test.go new file mode 100644 index 00000000..2a9992b8 --- /dev/null +++ b/core/v3/signature_build_test.go @@ -0,0 +1,68 @@ +package v3_test + +import ( + "bytes" + "context" + "math/big" + "testing" + + "github.com/0xsequence/ethkit/go-ethereum/common" + "github.com/0xsequence/go-sequence/core" + v3 "github.com/0xsequence/go-sequence/core/v3" + "github.com/stretchr/testify/require" +) + +// A subdigest leaf reports max signersWeight for any payload, so the config threshold +// looks met as soon as the first signature is collected. Early cancellation on that +// estimate nondeterministically drops the other signer's signature even though recovery +// of a payload not matching the subdigest still needs it. Every build must embed both +// signatures and produce identical bytes. +func TestBuildRegularSignatureCollectsAllSignersDespiteSubdigestLeaf(t *testing.T) { + signerA := common.HexToAddress("0x1111111111111111111111111111111111111111") + signerB := common.HexToAddress("0x2222222222222222222222222222222222222222") + + dummySignature := func(fill byte) []byte { + sig := make([]byte, 65) + for i := range 64 { + sig[i] = fill + } + sig[64] = 27 + return sig + } + signatureA := dummySignature(0xaa) + signatureB := dummySignature(0xbb) + + config := &v3.WalletConfig{ + Threshold_: 2, + Tree: v3.WalletConfigTreeNodes( + v3.WalletConfigTreeSubdigestLeaf{Subdigest: common.BigToHash(big.NewInt(1))}, + &v3.WalletConfigTreeAddressLeaf{Weight: 1, Address: signerA}, + &v3.WalletConfigTreeAddressLeaf{Weight: 1, Address: signerB}, + ), + } + + signatures := map[common.Address][]byte{ + signerA: signatureA, + signerB: signatureB, + } + signingFunc := func(ctx context.Context, signer core.Signer, _ []core.SignerSignature) (core.SignerSignatureType, []byte, error) { + if sig, ok := signatures[signer.Address]; ok { + return core.SignerSignatureTypeEthSign, sig, nil + } + return 0, nil, nil + } + + var first []byte + for range 100 { + sig, err := config.BuildRegularSignature(context.Background(), signingFunc, true) + require.NoError(t, err) + data, err := sig.Data() + require.NoError(t, err) + require.True(t, bytes.Contains(data, signatureA[:64]), "signer A's signature must be embedded on every build") + require.True(t, bytes.Contains(data, signatureB[:64]), "signer B's signature must be embedded on every build") + if first == nil { + first = data + } + require.Equal(t, first, data, "signature encoding must be deterministic") + } +} diff --git a/core/v3/v3.go b/core/v3/v3.go index e9574498..fee24d62 100644 --- a/core/v3/v3.go +++ b/core/v3/v3.go @@ -1999,7 +1999,6 @@ func (c *WalletConfig) BuildSubdigestSignature(noChainID bool) (core.Signature[* } func (c *WalletConfig) BuildRegularSignature(ctx context.Context, sign core.SigningFunction, validateSigningPower bool, checkpointerData ...[]byte) (core.Signature[*WalletConfig], error) { - var isValid bool configSigners := c.Signers() signCtx, signCancel := context.WithCancel(ctx) @@ -2020,15 +2019,18 @@ func (c *WalletConfig) BuildRegularSignature(ctx context.Context, sign core.Sign signerSignatures[signerSignature.Signer] = signerSignature signedSigners[signerSignature.Signer] = configSigners[signerSignature.Signer] - weight := c.Tree.signersWeight(signedSigners) + // Cancel outstanding signers only once collected signatures alone meet the + // threshold. signersWeight counts payload-blind subdigest leaves as satisfied, + // and cancelling on that estimate nondeterministically drops signatures that + // recovery of a non-matching payload still needs. + weight := c.Tree.collectedSignersWeight(signedSigners) if weight.Cmp(new(big.Int).SetUint64(uint64(c.Threshold_))) >= 0 { signCancel() - isValid = true } } } - if !isValid && validateSigningPower { + if validateSigningPower && c.Tree.signersWeight(signedSigners).Cmp(new(big.Int).SetUint64(uint64(c.Threshold_))) < 0 { return nil, fmt.Errorf("not enough signers to build regular signature") } @@ -2048,7 +2050,6 @@ func (c *WalletConfig) BuildRegularSignature(ctx context.Context, sign core.Sign } func (c *WalletConfig) BuildNoChainIDSignature(ctx context.Context, sign core.SigningFunction, validateSigningPower bool, checkpointerData ...[]byte) (core.Signature[*WalletConfig], error) { - var isValid bool configSigners := c.Signers() signCtx, signCancel := context.WithCancel(ctx) @@ -2069,15 +2070,18 @@ func (c *WalletConfig) BuildNoChainIDSignature(ctx context.Context, sign core.Si signerSignatures[signerSignature.Signer] = signerSignature signedSigners[signerSignature.Signer] = configSigners[signerSignature.Signer] - weight := c.Tree.signersWeight(signedSigners) + // Cancel outstanding signers only once collected signatures alone meet the + // threshold. signersWeight counts payload-blind subdigest leaves as satisfied, + // and cancelling on that estimate nondeterministically drops signatures that + // recovery of a non-matching payload still needs. + weight := c.Tree.collectedSignersWeight(signedSigners) if weight.Cmp(new(big.Int).SetUint64(uint64(c.Threshold_))) >= 0 { signCancel() - isValid = true } } } - if !isValid && validateSigningPower { + if validateSigningPower && c.Tree.signersWeight(signedSigners).Cmp(new(big.Int).SetUint64(uint64(c.Threshold_))) < 0 { return nil, fmt.Errorf("not enough signers to build no chain ID signature") } @@ -2096,36 +2100,17 @@ func (c *WalletConfig) BuildNoChainIDSignature(ctx context.Context, sign core.Si }}, nil } -// BuildRegularSignatureFromSignatures builds a regular signature directly from -// pre-collected signer signatures, with no signing orchestration. Use this instead of -// BuildRegularSignature when all signatures are already in hand: BuildRegularSignature -// cancels outstanding signers once the config threshold looks met, and payload-independent -// leaves (e.g. WalletConfigTreeAnyAddressSubdigestLeaf) can satisfy the threshold early, -// nondeterministically dropping supplied signatures that recovery still needs. Signers -// without a matching entry are encoded as their image hash; no signing power validation -// is performed. -func (c *WalletConfig) BuildRegularSignatureFromSignatures(signerSignatures map[core.Signer]core.SignerSignature, checkpointerData ...[]byte) core.Signature[*WalletConfig] { - var cpData []byte - if len(checkpointerData) > 0 { - cpData = checkpointerData[0] - } - - return &RegularSignature{&Signature{ - NoChainId: false, - Threshold: c.Threshold_, - Checkpoint: c.Checkpoint_, - Tree: c.Tree.buildSignatureTree(signerSignatures), - Checkpointer: c.Checkpointer, - CheckpointerData: cpData, - }} -} - type WalletConfigTree interface { core.ImageHashable isComplete() bool maxWeight() *big.Int signersWeight(signers map[core.Signer]uint16) *big.Int + // collectedSignersWeight is signersWeight with payload-blind leaves (subdigest + // leaves, which claim max weight for any payload) counting zero, so it only + // reflects weight from actually collected signatures. Safe to use for early + // cancellation decisions; signersWeight is not. + collectedSignersWeight(signers map[core.Signer]uint16) *big.Int readSignersIntoMap(signers map[core.Signer]uint16) buildSignatureTree(signerSignatures map[core.Signer]core.SignerSignature) signatureTree } @@ -2291,6 +2276,10 @@ func (n *WalletConfigTreeNode) signersWeight(signers map[core.Signer]uint16) *bi return new(big.Int).Add(n.Left.signersWeight(signers), n.Right.signersWeight(signers)) } +func (n *WalletConfigTreeNode) collectedSignersWeight(signers map[core.Signer]uint16) *big.Int { + return new(big.Int).Add(n.Left.collectedSignersWeight(signers), n.Right.collectedSignersWeight(signers)) +} + func (n *WalletConfigTreeNode) readSignersIntoMap(signers map[core.Signer]uint16) { n.Left.readSignersIntoMap(signers) n.Right.readSignersIntoMap(signers) @@ -2369,6 +2358,10 @@ func (l *WalletConfigTreeAddressLeaf) signersWeight(signers map[core.Signer]uint } } +func (l *WalletConfigTreeAddressLeaf) collectedSignersWeight(signers map[core.Signer]uint16) *big.Int { + return l.signersWeight(signers) +} + func (l *WalletConfigTreeAddressLeaf) readSignersIntoMap(signers map[core.Signer]uint16) { signers[core.Signer{Address: l.Address}] = uint16(l.Weight) } @@ -2466,6 +2459,10 @@ func (l WalletConfigTreeNodeLeaf) signersWeight(signers map[core.Signer]uint16) return new(big.Int) } +func (l WalletConfigTreeNodeLeaf) collectedSignersWeight(signers map[core.Signer]uint16) *big.Int { + return new(big.Int) +} + func (l WalletConfigTreeNodeLeaf) readSignersIntoMap(signers map[core.Signer]uint16) { } @@ -2553,6 +2550,13 @@ func (l *WalletConfigTreeNestedLeaf) signersWeight(signers map[core.Signer]uint1 return new(big.Int) } +func (l *WalletConfigTreeNestedLeaf) collectedSignersWeight(signers map[core.Signer]uint16) *big.Int { + if l.Tree.collectedSignersWeight(signers).Cmp(new(big.Int).SetUint64(uint64(l.Threshold))) >= 0 { + return new(big.Int).SetUint64(uint64(l.Weight)) + } + return new(big.Int) +} + func (l *WalletConfigTreeNestedLeaf) readSignersIntoMap(signers map[core.Signer]uint16) { l.Tree.readSignersIntoMap(signers) } @@ -2619,6 +2623,10 @@ func (l WalletConfigTreeSubdigestLeaf) signersWeight(signers map[core.Signer]uin return new(big.Int).Set(maxUint256) } +func (l WalletConfigTreeSubdigestLeaf) collectedSignersWeight(signers map[core.Signer]uint16) *big.Int { + return new(big.Int) +} + func (l WalletConfigTreeSubdigestLeaf) readSignersIntoMap(signers map[core.Signer]uint16) { } @@ -2709,6 +2717,10 @@ func (l *WalletConfigTreeSapientSignerLeaf) signersWeight(signers map[core.Signe } } +func (l *WalletConfigTreeSapientSignerLeaf) collectedSignersWeight(signers map[core.Signer]uint16) *big.Int { + return l.signersWeight(signers) +} + func (l *WalletConfigTreeSapientSignerLeaf) readSignersIntoMap(signers map[core.Signer]uint16) { signers[core.SapientSigner(l.Address, l.ImageHash_.Hash)] = uint16(l.Weight) } @@ -2785,6 +2797,10 @@ func (l WalletConfigTreeAnyAddressSubdigestLeaf) signersWeight(signers map[core. return new(big.Int).Set(maxUint256) } +func (l WalletConfigTreeAnyAddressSubdigestLeaf) collectedSignersWeight(signers map[core.Signer]uint16) *big.Int { + return new(big.Int) +} + func (l WalletConfigTreeAnyAddressSubdigestLeaf) readSignersIntoMap(signers map[core.Signer]uint16) { } diff --git a/intent_config.go b/intent_config.go index f7687a8f..993cf096 100644 --- a/intent_config.go +++ b/intent_config.go @@ -1,6 +1,7 @@ package sequence import ( + "context" "fmt" "math/big" @@ -379,23 +380,28 @@ func CreateIntentConfiguration( } // `BuildIntentConfigurationSignature` creates a signature for an already-built intent configuration -// that can be used to bypass chain ID validation. All supplied signer signatures are -// embedded deterministically; signers without a supplied signature are encoded as their -// image hash. +// that can be used to bypass chain ID validation. func BuildIntentConfigurationSignature(config *v3.WalletConfig, signerSignatures []*core.SignerSignature) ([]byte, error) { if config == nil { return nil, fmt.Errorf("intent configuration is nil") } - signatures := make(map[core.Signer]core.SignerSignature, len(signerSignatures)) - for _, signerSignature := range signerSignatures { - if signerSignature != nil { - signatures[signerSignature.Signer] = *signerSignature + signingFunc := func(ctx context.Context, signer core.Signer, _ []core.SignerSignature) (core.SignerSignatureType, []byte, error) { + for _, signerSignature := range signerSignatures { + if signer == signerSignature.Signer { + return signerSignature.Type, signerSignature.Signature, nil + } } + return 0, nil, nil } - sig := config.BuildRegularSignatureFromSignatures(signatures) + // Set validateSigningPower to false, as we are not necessarily providing signatures for all parts of the config. + sig, err := config.BuildRegularSignature(context.Background(), signingFunc, false) + if err != nil { + return nil, fmt.Errorf("failed to build regular signature: %w", err) + } + // Get the signature data data, err := sig.Data() if err != nil { return nil, fmt.Errorf("failed to get signature data: %w", err) From df05cc4cddc18077f560ce3ca33ab37b153c3033 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Fri, 31 Jul 2026 12:51:47 +1200 Subject: [PATCH 2/4] Add nil check --- intent_config.go | 2 +- intent_config_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/intent_config.go b/intent_config.go index 993cf096..80c44fe3 100644 --- a/intent_config.go +++ b/intent_config.go @@ -388,7 +388,7 @@ func BuildIntentConfigurationSignature(config *v3.WalletConfig, signerSignatures signingFunc := func(ctx context.Context, signer core.Signer, _ []core.SignerSignature) (core.SignerSignatureType, []byte, error) { for _, signerSignature := range signerSignatures { - if signer == signerSignature.Signer { + if signerSignature != nil && signer == signerSignature.Signer { return signerSignature.Type, signerSignature.Signature, nil } } diff --git a/intent_config_test.go b/intent_config_test.go index b1be9485..7737230a 100644 --- a/intent_config_test.go +++ b/intent_config_test.go @@ -409,6 +409,32 @@ func TestCreateIntentConfigurationWithTimedRefundSapient(t *testing.T) { require.NotEqual(t, plainSignature, signature) } +// Nil entries in signerSignatures are accepted and ignored, not dereferenced. +func TestBuildIntentConfigurationSignatureNilSignerSignatureIgnored(t *testing.T) { + payload := v3.NewCallsPayload(common.Address{}, testChain.ChainID(), []v3.Call{ + { + To: common.HexToAddress("0x1111111111111111111111111111111111111111"), + Value: nil, + Data: []byte{0x12, 0x34}, + GasLimit: big.NewInt(0), + DelegateCall: false, + OnlyFallback: false, + BehaviorOnError: v3.BehaviorOnErrorRevert, + }, + }, big.NewInt(0), big.NewInt(0)) + mainSigner := common.HexToAddress("0x2222222222222222222222222222222222222222") + + config, err := sequence.CreateIntentConfiguration(mainSigner, []*v3.CallsPayload{&payload}, 0) + require.NoError(t, err) + + signatureWithNilEntry, err := sequence.BuildIntentConfigurationSignature(config, []*core.SignerSignature{nil}) + require.NoError(t, err) + + signatureWithoutEntries, err := sequence.BuildIntentConfigurationSignature(config, nil) + require.NoError(t, err) + require.Equal(t, signatureWithoutEntries, signatureWithNilEntry) +} + // With gateLeaf nil (the default/legacy case), the tree must keep the exact flat // shape it had before this parameter existed: Node(mainSignerLeaf, Node(subdigestLeaf, // additionalLeaf)) — no extra nesting — so already-derived counterfactual addresses do From 5ea1ce2c5896f70ea11b8ad11d2690c0669ba587 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Fri, 31 Jul 2026 13:03:36 +1200 Subject: [PATCH 3/4] fix: require a collected signature before signing-power validation passes The final signersWeight check counts payload-blind subdigest leaves as satisfied even for an empty signer set, so BuildRegularSignature and BuildNoChainIDSignature with validateSigningPower could succeed without collecting any signature. Restore the isValid flag, set only after a non-nil signature brings the optimistic signersWeight to the threshold, while cancellation keeps using collectedSignersWeight. Co-Authored-By: Claude Fable 5 --- core/v3/signature_build_test.go | 22 ++++++++++++++++++++++ core/v3/v3.go | 22 ++++++++++++++++------ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/core/v3/signature_build_test.go b/core/v3/signature_build_test.go index 2a9992b8..cfe66f9d 100644 --- a/core/v3/signature_build_test.go +++ b/core/v3/signature_build_test.go @@ -66,3 +66,25 @@ func TestBuildRegularSignatureCollectsAllSignersDespiteSubdigestLeaf(t *testing. require.Equal(t, first, data, "signature encoding must be deterministic") } } + +// A subdigest leaf reports max signersWeight even for an empty signer set, so +// signing-power validation must still fail when no signature is collected at all. +func TestBuildSignatureValidationRejectsEmptySignerSetDespiteSubdigestLeaf(t *testing.T) { + config := &v3.WalletConfig{ + Threshold_: 2, + Tree: v3.WalletConfigTreeNodes( + v3.WalletConfigTreeSubdigestLeaf{Subdigest: common.BigToHash(big.NewInt(1))}, + &v3.WalletConfigTreeAddressLeaf{Weight: 1, Address: common.HexToAddress("0x1111111111111111111111111111111111111111")}, + ), + } + + signingFunc := func(ctx context.Context, signer core.Signer, _ []core.SignerSignature) (core.SignerSignatureType, []byte, error) { + return 0, nil, nil + } + + _, err := config.BuildRegularSignature(context.Background(), signingFunc, true) + require.ErrorContains(t, err, "not enough signers") + + _, err = config.BuildNoChainIDSignature(context.Background(), signingFunc, true) + require.ErrorContains(t, err, "not enough signers") +} diff --git a/core/v3/v3.go b/core/v3/v3.go index fee24d62..87469531 100644 --- a/core/v3/v3.go +++ b/core/v3/v3.go @@ -1999,7 +1999,9 @@ func (c *WalletConfig) BuildSubdigestSignature(noChainID bool) (core.Signature[* } func (c *WalletConfig) BuildRegularSignature(ctx context.Context, sign core.SigningFunction, validateSigningPower bool, checkpointerData ...[]byte) (core.Signature[*WalletConfig], error) { + var isValid bool configSigners := c.Signers() + threshold := new(big.Int).SetUint64(uint64(c.Threshold_)) signCtx, signCancel := context.WithCancel(ctx) defer signCancel() @@ -2023,14 +2025,17 @@ func (c *WalletConfig) BuildRegularSignature(ctx context.Context, sign core.Sign // threshold. signersWeight counts payload-blind subdigest leaves as satisfied, // and cancelling on that estimate nondeterministically drops signatures that // recovery of a non-matching payload still needs. - weight := c.Tree.collectedSignersWeight(signedSigners) - if weight.Cmp(new(big.Int).SetUint64(uint64(c.Threshold_))) >= 0 { + if c.Tree.collectedSignersWeight(signedSigners).Cmp(threshold) >= 0 { signCancel() } + + if c.Tree.signersWeight(signedSigners).Cmp(threshold) >= 0 { + isValid = true + } } } - if validateSigningPower && c.Tree.signersWeight(signedSigners).Cmp(new(big.Int).SetUint64(uint64(c.Threshold_))) < 0 { + if !isValid && validateSigningPower { return nil, fmt.Errorf("not enough signers to build regular signature") } @@ -2050,7 +2055,9 @@ func (c *WalletConfig) BuildRegularSignature(ctx context.Context, sign core.Sign } func (c *WalletConfig) BuildNoChainIDSignature(ctx context.Context, sign core.SigningFunction, validateSigningPower bool, checkpointerData ...[]byte) (core.Signature[*WalletConfig], error) { + var isValid bool configSigners := c.Signers() + threshold := new(big.Int).SetUint64(uint64(c.Threshold_)) signCtx, signCancel := context.WithCancel(ctx) defer signCancel() @@ -2074,14 +2081,17 @@ func (c *WalletConfig) BuildNoChainIDSignature(ctx context.Context, sign core.Si // threshold. signersWeight counts payload-blind subdigest leaves as satisfied, // and cancelling on that estimate nondeterministically drops signatures that // recovery of a non-matching payload still needs. - weight := c.Tree.collectedSignersWeight(signedSigners) - if weight.Cmp(new(big.Int).SetUint64(uint64(c.Threshold_))) >= 0 { + if c.Tree.collectedSignersWeight(signedSigners).Cmp(threshold) >= 0 { signCancel() } + + if c.Tree.signersWeight(signedSigners).Cmp(threshold) >= 0 { + isValid = true + } } } - if validateSigningPower && c.Tree.signersWeight(signedSigners).Cmp(new(big.Int).SetUint64(uint64(c.Threshold_))) < 0 { + if !isValid && validateSigningPower { return nil, fmt.Errorf("not enough signers to build no chain ID signature") } From db7c47244ded53a6e7a1ff288cae5014c0dcd109 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Fri, 31 Jul 2026 13:40:16 +1200 Subject: [PATCH 4/4] fix: keep pre-collected intent signatures via FromSignatures Routing through BuildRegularSignature cancelled early once the main signer met threshold 1. Restore the no-orchestration path and drop the intent-only churn; collectedSignersWeight remains the fix for live signing cancellation. Co-authored-by: Cursor --- core/v3/v3.go | 24 ++++++++++++++++++++++++ intent_config.go | 22 ++++++++-------------- intent_config_test.go | 26 -------------------------- 3 files changed, 32 insertions(+), 40 deletions(-) diff --git a/core/v3/v3.go b/core/v3/v3.go index 87469531..3f172186 100644 --- a/core/v3/v3.go +++ b/core/v3/v3.go @@ -2110,6 +2110,30 @@ func (c *WalletConfig) BuildNoChainIDSignature(ctx context.Context, sign core.Si }}, nil } +// BuildRegularSignatureFromSignatures builds a regular signature directly from +// pre-collected signer signatures, with no signing orchestration. Use this instead of +// BuildRegularSignature when all signatures are already in hand: BuildRegularSignature +// cancels outstanding signers once the config threshold looks met, and payload-independent +// leaves (e.g. WalletConfigTreeAnyAddressSubdigestLeaf) can satisfy the threshold early, +// nondeterministically dropping supplied signatures that recovery still needs. Signers +// without a matching entry are encoded as their image hash; no signing power validation +// is performed. +func (c *WalletConfig) BuildRegularSignatureFromSignatures(signerSignatures map[core.Signer]core.SignerSignature, checkpointerData ...[]byte) core.Signature[*WalletConfig] { + var cpData []byte + if len(checkpointerData) > 0 { + cpData = checkpointerData[0] + } + + return &RegularSignature{&Signature{ + NoChainId: false, + Threshold: c.Threshold_, + Checkpoint: c.Checkpoint_, + Tree: c.Tree.buildSignatureTree(signerSignatures), + Checkpointer: c.Checkpointer, + CheckpointerData: cpData, + }} +} + type WalletConfigTree interface { core.ImageHashable diff --git a/intent_config.go b/intent_config.go index 80c44fe3..f7687a8f 100644 --- a/intent_config.go +++ b/intent_config.go @@ -1,7 +1,6 @@ package sequence import ( - "context" "fmt" "math/big" @@ -380,28 +379,23 @@ func CreateIntentConfiguration( } // `BuildIntentConfigurationSignature` creates a signature for an already-built intent configuration -// that can be used to bypass chain ID validation. +// that can be used to bypass chain ID validation. All supplied signer signatures are +// embedded deterministically; signers without a supplied signature are encoded as their +// image hash. func BuildIntentConfigurationSignature(config *v3.WalletConfig, signerSignatures []*core.SignerSignature) ([]byte, error) { if config == nil { return nil, fmt.Errorf("intent configuration is nil") } - signingFunc := func(ctx context.Context, signer core.Signer, _ []core.SignerSignature) (core.SignerSignatureType, []byte, error) { - for _, signerSignature := range signerSignatures { - if signerSignature != nil && signer == signerSignature.Signer { - return signerSignature.Type, signerSignature.Signature, nil - } + signatures := make(map[core.Signer]core.SignerSignature, len(signerSignatures)) + for _, signerSignature := range signerSignatures { + if signerSignature != nil { + signatures[signerSignature.Signer] = *signerSignature } - return 0, nil, nil } - // Set validateSigningPower to false, as we are not necessarily providing signatures for all parts of the config. - sig, err := config.BuildRegularSignature(context.Background(), signingFunc, false) - if err != nil { - return nil, fmt.Errorf("failed to build regular signature: %w", err) - } + sig := config.BuildRegularSignatureFromSignatures(signatures) - // Get the signature data data, err := sig.Data() if err != nil { return nil, fmt.Errorf("failed to get signature data: %w", err) diff --git a/intent_config_test.go b/intent_config_test.go index 7737230a..b1be9485 100644 --- a/intent_config_test.go +++ b/intent_config_test.go @@ -409,32 +409,6 @@ func TestCreateIntentConfigurationWithTimedRefundSapient(t *testing.T) { require.NotEqual(t, plainSignature, signature) } -// Nil entries in signerSignatures are accepted and ignored, not dereferenced. -func TestBuildIntentConfigurationSignatureNilSignerSignatureIgnored(t *testing.T) { - payload := v3.NewCallsPayload(common.Address{}, testChain.ChainID(), []v3.Call{ - { - To: common.HexToAddress("0x1111111111111111111111111111111111111111"), - Value: nil, - Data: []byte{0x12, 0x34}, - GasLimit: big.NewInt(0), - DelegateCall: false, - OnlyFallback: false, - BehaviorOnError: v3.BehaviorOnErrorRevert, - }, - }, big.NewInt(0), big.NewInt(0)) - mainSigner := common.HexToAddress("0x2222222222222222222222222222222222222222") - - config, err := sequence.CreateIntentConfiguration(mainSigner, []*v3.CallsPayload{&payload}, 0) - require.NoError(t, err) - - signatureWithNilEntry, err := sequence.BuildIntentConfigurationSignature(config, []*core.SignerSignature{nil}) - require.NoError(t, err) - - signatureWithoutEntries, err := sequence.BuildIntentConfigurationSignature(config, nil) - require.NoError(t, err) - require.Equal(t, signatureWithoutEntries, signatureWithNilEntry) -} - // With gateLeaf nil (the default/legacy case), the tree must keep the exact flat // shape it had before this parameter existed: Node(mainSignerLeaf, Node(subdigestLeaf, // additionalLeaf)) — no extra nesting — so already-derived counterfactual addresses do