From a1766ec58de515efc92564f8b0847343d248f0dc Mon Sep 17 00:00:00 2001 From: "Andrew Camilleri (Kukks)" Date: Fri, 16 Oct 2026 11:41:44 +0200 Subject: [PATCH 1/2] fix(template): accept array covenant inputs in artifacts arkadec emits bytes[3] for a bytes[N] function parameter. Parse it like a constructor array; refuse a template that would spend such a function, since the instance fills one stack item per input. --- pkg/template/artifact.go | 2 +- pkg/template/artifact_test.go | 16 ++++++++++++++++ pkg/template/template.go | 5 +++++ pkg/template/template_test.go | 19 +++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/pkg/template/artifact.go b/pkg/template/artifact.go index 39c0131..94008d8 100644 --- a/pkg/template/artifact.go +++ b/pkg/template/artifact.go @@ -220,7 +220,7 @@ func parseFunction(v any) (*Function, error) { return nil, err } for _, in := range c.Inputs { - if !slices.Contains(scalarTypes, in.Type) { + if !validType(in.Type, nil) { return nil, invalid("unsupported arkade input type %q", in.Type) } } diff --git a/pkg/template/artifact_test.go b/pkg/template/artifact_test.go index 67fd389..950cf52 100644 --- a/pkg/template/artifact_test.go +++ b/pkg/template/artifact_test.go @@ -267,3 +267,19 @@ func fnOf(m map[string]any, i int) map[string]any { func leafOf(m map[string]any, f int) map[string]any { return fnOf(m, f)["leaves"].([]any)[0].(map[string]any) } + +func TestParseArtifactArrayInput(t *testing.T) { + doc := func(typ string) string { + return `{"contractName":"C","constructorInputs":[{"name":"k","type":"pubkey"}],"functions":[{"name":"f", + "arkade":{"inputs":[{"name":"sigs","type":"` + typ + `"}],"asm":["","OP_CHECKSIG"]}, + "leaves":[{"name":"l","witness":[{"name":"s","type":"signature","encoding":"schnorr-64","injected":true}], + "asm":["","OP_CHECKSIGVERIFY","","OP_CHECKSIG"]}]}]}` + } + a, err := ParseArtifact([]byte(doc("bytes[3]"))) + require.NoError(t, err) + require.Equal(t, "bytes[3]", a.Definition.Functions[0].Arkade.Inputs[0].Type) + for _, typ := range []string{"float[3]", "bytes[0]", "bytes[]", "bytes[3][2]", "signature[3]"} { + _, err := ParseArtifact([]byte(doc(typ))) + require.ErrorIs(t, err, ErrInvalidArtifact, typ) + } +} diff --git a/pkg/template/template.go b/pkg/template/template.go index 406acbf..d45cb4f 100644 --- a/pkg/template/template.go +++ b/pkg/template/template.go @@ -536,6 +536,11 @@ func (p *parser) spend(in *input, v any) error { if f.Arkade != nil { want = f.Arkade.Inputs } + for _, w := range want { + if arrayType.MatchString(w.Type) { + return fmt.Errorf("%w: function %q takes the array input %q, which a template cannot fill", ErrUnsupported, f.Name, w.Name) + } + } if len(args) != len(want) { return bad("function %q takes %d arguments, got %d", f.Name, len(want), len(args)) } diff --git a/pkg/template/template_test.go b/pkg/template/template_test.go index 9570489..ad35c7b 100644 --- a/pkg/template/template_test.go +++ b/pkg/template/template_test.go @@ -697,3 +697,22 @@ const vhtlc = ` "packets": {"output_index": 1, "unmatched": "drop"} } ` + +func TestArrayInputs(t *testing.T) { + withInput := func(fn int) []byte { + return edited(t, []byte(releaseDoc), func(m map[string]any) { + def := m["inputs"].([]any)[0].(map[string]any)["contract"].(map[string]any)["definition"].(map[string]any) + f := def["functions"].([]any)[fn].(map[string]any) + if f["arkade"] == nil { + f["arkade"] = map[string]any{"asm": []any{"1"}} + } + f["arkade"].(map[string]any)["inputs"] = []any{map[string]any{"name": "sigs", "type": "bytes[3]"}} + }) + } + _, err := Parse(t.Context(), withInput(0), nil) + require.NoError(t, err, "an array input on a function the template does not spend") + + _, err = Parse(t.Context(), withInput(1), nil) + require.ErrorIs(t, err, ErrUnsupported) + require.ErrorContains(t, err, `array input "sigs"`) +} From cc0b7c1e6c6fd025d56d35c2370abe836e55c2b8 Mon Sep 17 00:00:00 2001 From: "Andrew Camilleri (Kukks)" Date: Fri, 16 Oct 2026 12:55:15 +0200 Subject: [PATCH 2/2] test(template): pick TestArrayInputs functions by name The spent and unspent functions were addressed by index into releaseDoc, so reordering its functions would silently swap what each half of the test checks. --- pkg/template/template_test.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pkg/template/template_test.go b/pkg/template/template_test.go index ad35c7b..283aaae 100644 --- a/pkg/template/template_test.go +++ b/pkg/template/template_test.go @@ -699,20 +699,25 @@ const vhtlc = ` ` func TestArrayInputs(t *testing.T) { - withInput := func(fn int) []byte { + withInput := func(name string) []byte { return edited(t, []byte(releaseDoc), func(m map[string]any) { def := m["inputs"].([]any)[0].(map[string]any)["contract"].(map[string]any)["definition"].(map[string]any) - f := def["functions"].([]any)[fn].(map[string]any) - if f["arkade"] == nil { - f["arkade"] = map[string]any{"asm": []any{"1"}} + for _, raw := range def["functions"].([]any) { + if f := raw.(map[string]any); f["name"] == name { + if f["arkade"] == nil { + f["arkade"] = map[string]any{"asm": []any{"1"}} + } + f["arkade"].(map[string]any)["inputs"] = []any{map[string]any{"name": "sigs", "type": "bytes[3]"}} + return + } } - f["arkade"].(map[string]any)["inputs"] = []any{map[string]any{"name": "sigs", "type": "bytes[3]"}} + t.Fatalf("releaseDoc has no function %q", name) }) } - _, err := Parse(t.Context(), withInput(0), nil) + _, err := Parse(t.Context(), withInput("exit"), nil) require.NoError(t, err, "an array input on a function the template does not spend") - _, err = Parse(t.Context(), withInput(1), nil) + _, err = Parse(t.Context(), withInput("release"), nil) require.ErrorIs(t, err, ErrUnsupported) require.ErrorContains(t, err, `array input "sigs"`) }