Skip to content

Commit eeb1fd5

Browse files
committed
feat: multi-sig script (CIP-1854)
Signed-off-by: Chris Gianelloni <[email protected]>
1 parent 72ceac7 commit eeb1fd5

File tree

12 files changed

+2641
-49
lines changed

12 files changed

+2641
-49
lines changed

bursa.go

Lines changed: 705 additions & 0 deletions
Large diffs are not rendered by default.

bursa_test.go

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package bursa
1616

1717
import (
18+
"crypto/ed25519"
1819
"encoding/hex"
1920
"errors"
2021
"io/fs"
@@ -877,3 +878,248 @@ func TestLoadWalletDirValidation(t *testing.T) {
877878
})
878879
}
879880
}
881+
882+
func TestScriptTypes(t *testing.T) {
883+
// Test script sig
884+
keyHash := []byte{
885+
0x01,
886+
0x02,
887+
0x03,
888+
0x04,
889+
0x05,
890+
0x06,
891+
0x07,
892+
0x08,
893+
0x09,
894+
0x0a,
895+
0x0b,
896+
0x0c,
897+
0x0d,
898+
0x0e,
899+
0x0f,
900+
0x10,
901+
0x11,
902+
0x12,
903+
0x13,
904+
0x14,
905+
0x15,
906+
0x16,
907+
0x17,
908+
0x18,
909+
0x19,
910+
0x1a,
911+
0x1b,
912+
0x1c,
913+
}
914+
scriptSig := NewScriptSig(keyHash)
915+
assert.Equal(t, 0, scriptSig.Type())
916+
cbor, err := scriptSig.CBOR()
917+
assert.NoError(t, err)
918+
assert.NotEmpty(t, cbor)
919+
920+
// Test script all
921+
scriptAll := NewScriptAll(scriptSig)
922+
assert.Equal(t, 1, scriptAll.Type())
923+
cbor, err = scriptAll.CBOR()
924+
assert.NoError(t, err)
925+
assert.NotEmpty(t, cbor)
926+
927+
// Test script any
928+
scriptAny := NewScriptAny(scriptSig)
929+
assert.Equal(t, 2, scriptAny.Type())
930+
cbor, err = scriptAny.CBOR()
931+
assert.NoError(t, err)
932+
assert.NotEmpty(t, cbor)
933+
934+
// Test script N-of
935+
scriptNOf := NewScriptNOf(2, scriptSig, scriptSig)
936+
assert.Equal(t, 3, scriptNOf.Type())
937+
cbor, err = scriptNOf.CBOR()
938+
assert.NoError(t, err)
939+
assert.NotEmpty(t, cbor)
940+
941+
// Test script before
942+
scriptBefore := NewScriptBefore(123456789)
943+
assert.Equal(t, 4, scriptBefore.Type())
944+
cbor, err = scriptBefore.CBOR()
945+
assert.NoError(t, err)
946+
assert.NotEmpty(t, cbor)
947+
948+
// Test script after
949+
scriptAfter := NewScriptAfter(123456789)
950+
assert.Equal(t, 5, scriptAfter.Type())
951+
cbor, err = scriptAfter.CBOR()
952+
assert.NoError(t, err)
953+
assert.NotEmpty(t, cbor)
954+
}
955+
956+
func TestGetScriptHash(t *testing.T) {
957+
script := NewScriptSig([]byte{0x01, 0x02, 0x03})
958+
hash, err := GetScriptHash(script)
959+
assert.NoError(t, err)
960+
assert.Len(t, hash, 28)
961+
}
962+
963+
func TestGetScriptAddress(t *testing.T) {
964+
script := NewScriptSig([]byte{0x01, 0x02, 0x03})
965+
addr, err := GetScriptAddress(script, "mainnet")
966+
assert.NoError(t, err)
967+
assert.NotEmpty(t, addr)
968+
assert.True(t, strings.HasPrefix(addr, "addr1"))
969+
970+
// Test invalid network
971+
_, err = GetScriptAddress(script, "invalid")
972+
assert.Error(t, err)
973+
assert.Equal(t, ErrInvalidNetwork, err)
974+
}
975+
976+
func TestMultiSigKeyDerivation(t *testing.T) {
977+
mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
978+
rootKey, err := GetRootKeyFromMnemonic(mnemonic, "")
979+
assert.NoError(t, err)
980+
981+
accountKey, err := GetMultiSigAccountKey(rootKey, 0)
982+
assert.NoError(t, err)
983+
984+
paymentKey, err := GetMultiSigPaymentKey(accountKey, 0)
985+
assert.NoError(t, err)
986+
987+
vkey, err := GetMultiSigPaymentVKey(paymentKey)
988+
assert.NoError(t, err)
989+
assert.Equal(t, "PaymentVerificationKeyShelley_ed25519", vkey.Type)
990+
991+
skey, err := GetMultiSigPaymentSKey(paymentKey)
992+
assert.NoError(t, err)
993+
assert.Equal(t, "PaymentSigningKeyShelley_ed25519", skey.Type)
994+
995+
stakeKey, err := GetMultiSigStakeKey(accountKey, 0)
996+
assert.NoError(t, err)
997+
998+
stakeVKey, err := GetMultiSigStakeVKey(stakeKey)
999+
assert.NoError(t, err)
1000+
assert.Equal(t, "StakeVerificationKeyShelley_ed25519", stakeVKey.Type)
1001+
1002+
stakeSKey, err := GetMultiSigStakeSKey(stakeKey)
1003+
assert.NoError(t, err)
1004+
assert.Equal(t, "StakeSigningKeyShelley_ed25519", stakeSKey.Type)
1005+
}
1006+
1007+
func TestValidateScript(t *testing.T) {
1008+
// Test ScriptSig validation (allows empty signatures for basic validation)
1009+
scriptSig := NewScriptSig([]byte{0x01, 0x02, 0x03})
1010+
assert.True(t, ValidateScript(scriptSig, nil, 1000))
1011+
1012+
// Test ScriptAll validation (requires all sub-scripts satisfied)
1013+
// Since ScriptSig allows empty signatures, ScriptAll should succeed
1014+
scriptAll := NewScriptAll(
1015+
NewScriptSig([]byte{0x01}),
1016+
NewScriptSig([]byte{0x02}),
1017+
)
1018+
assert.True(t, ValidateScript(scriptAll, nil, 1000))
1019+
1020+
// Test ScriptAny validation (requires any sub-script satisfied)
1021+
// Since ScriptSig allows empty signatures, ScriptAny should succeed
1022+
scriptAny := NewScriptAny(
1023+
NewScriptSig([]byte{0x01}),
1024+
NewScriptSig([]byte{0x02}),
1025+
)
1026+
assert.True(t, ValidateScript(scriptAny, nil, 1000))
1027+
1028+
// Test ScriptNOf validation (2-of-3)
1029+
// Since ScriptSig allows empty signatures, ScriptNOf should succeed
1030+
scriptNOf := NewScriptNOf(2,
1031+
NewScriptSig([]byte{0x01}),
1032+
NewScriptSig([]byte{0x02}),
1033+
NewScriptSig([]byte{0x03}),
1034+
)
1035+
assert.True(t, ValidateScript(scriptNOf, nil, 1000))
1036+
1037+
// Test ScriptBefore validation
1038+
scriptBefore := NewScriptBefore(2000)
1039+
assert.True(t, ValidateScript(scriptBefore, nil, 1000)) // Slot 1000 < 2000
1040+
assert.False(t, ValidateScript(scriptBefore, nil, 3000)) // Slot 3000 > 2000
1041+
1042+
// Test ScriptAfter validation
1043+
scriptAfter := NewScriptAfter(1000)
1044+
assert.True(t, ValidateScript(scriptAfter, nil, 2000)) // Slot 2000 > 1000
1045+
assert.False(t, ValidateScript(scriptAfter, nil, 500)) // Slot 500 < 1000
1046+
}
1047+
1048+
func TestMultiSigScriptGeneration(t *testing.T) {
1049+
keyHash1 := []byte{0x01, 0x02, 0x03}
1050+
keyHash2 := []byte{0x04, 0x05, 0x06}
1051+
keyHash3 := []byte{0x07, 0x08, 0x09}
1052+
1053+
// Test NewMultiSigScript (2-of-3)
1054+
script2of3, err := NewMultiSigScript(2, keyHash1, keyHash2, keyHash3)
1055+
assert.NoError(t, err)
1056+
assert.Equal(t, 3, script2of3.Type()) // NOf type
1057+
assert.Equal(t, 2, script2of3.N)
1058+
assert.Len(t, script2of3.Scripts, 3)
1059+
1060+
// Test NewAllMultiSigScript
1061+
scriptAll, err := NewAllMultiSigScript(keyHash1, keyHash2)
1062+
assert.NoError(t, err)
1063+
assert.Equal(t, 1, scriptAll.Type()) // All type
1064+
assert.Len(t, scriptAll.Scripts, 2)
1065+
1066+
// Test NewAnyMultiSigScript
1067+
scriptAny, err := NewAnyMultiSigScript(keyHash1, keyHash2, keyHash3)
1068+
assert.NoError(t, err)
1069+
assert.Equal(t, 2, scriptAny.Type()) // Any type
1070+
assert.Len(t, scriptAny.Scripts, 3)
1071+
1072+
// Test NewTimelockedScript (before)
1073+
timelockedBefore := NewTimelockedScript(1000, true, scriptAll)
1074+
assert.Equal(t, 1, timelockedBefore.Type()) // All type (wrapping)
1075+
allScript := timelockedBefore.(*ScriptAll)
1076+
assert.Len(t, allScript.Scripts, 2) // Before script + original script
1077+
1078+
// Test NewTimelockedScript (after)
1079+
timelockedAfter := NewTimelockedScript(2000, false, scriptAny)
1080+
assert.Equal(t, 1, timelockedAfter.Type()) // All type (wrapping)
1081+
allScriptAfter := timelockedAfter.(*ScriptAll)
1082+
assert.Len(t, allScriptAfter.Scripts, 2) // After script + original script
1083+
}
1084+
1085+
func TestMultiSigScriptFromKeys(t *testing.T) {
1086+
// Create some dummy Ed25519 public keys
1087+
pubKey1, _, _ := ed25519.GenerateKey(nil)
1088+
pubKey2, _, _ := ed25519.GenerateKey(nil)
1089+
pubKey3, _, _ := ed25519.GenerateKey(nil)
1090+
1091+
// Test creating 2-of-3 script from public keys
1092+
script, err := NewMultiSigScriptFromKeys(2, pubKey1, pubKey2, pubKey3)
1093+
assert.NoError(t, err)
1094+
assert.Equal(t, 3, script.Type()) // NOf type
1095+
assert.Equal(t, 2, script.N)
1096+
assert.Len(t, script.Scripts, 3)
1097+
1098+
// Verify the script can be validated
1099+
assert.True(t, ValidateScript(script, nil, 1000))
1100+
}
1101+
1102+
func TestScriptGenerationEdgeCases(t *testing.T) {
1103+
keyHash := []byte{0x01, 0x02, 0x03}
1104+
1105+
// Test invalid parameters (should return errors)
1106+
_, err := NewMultiSigScript(0, keyHash)
1107+
assert.Error(t, err)
1108+
assert.Contains(t, err.Error(), "invalid required signatures count")
1109+
1110+
_, err = NewMultiSigScript(2, keyHash)
1111+
assert.Error(t, err)
1112+
assert.Contains(t, err.Error(), "invalid required signatures count")
1113+
1114+
_, err = NewMultiSigScript(1)
1115+
assert.Error(t, err)
1116+
assert.Contains(t, err.Error(), "at least one key hash required")
1117+
1118+
_, err = NewAllMultiSigScript()
1119+
assert.Error(t, err)
1120+
assert.Contains(t, err.Error(), "at least one key hash required")
1121+
1122+
_, err = NewAnyMultiSigScript()
1123+
assert.Error(t, err)
1124+
assert.Contains(t, err.Error(), "at least one key hash required")
1125+
}

cmd/bursa/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func main() {
3939
rootCmd.AddCommand(
4040
walletCommand(),
4141
apiCommand(),
42+
scriptCommand(),
4243
)
4344

4445
if err := rootCmd.Execute(); err != nil {

0 commit comments

Comments
 (0)