Skip to content

Commit fd1c535

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

File tree

12 files changed

+2450
-49
lines changed

12 files changed

+2450
-49
lines changed

bursa.go

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

bursa_test.go

Lines changed: 224 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,226 @@ 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 := scriptSig.CBOR()
917+
assert.NotEmpty(t, cbor)
918+
919+
// Test script all
920+
scriptAll := NewScriptAll(scriptSig)
921+
assert.Equal(t, 1, scriptAll.Type())
922+
cbor = scriptAll.CBOR()
923+
assert.NotEmpty(t, cbor)
924+
925+
// Test script any
926+
scriptAny := NewScriptAny(scriptSig)
927+
assert.Equal(t, 2, scriptAny.Type())
928+
cbor = scriptAny.CBOR()
929+
assert.NotEmpty(t, cbor)
930+
931+
// Test script N-of
932+
scriptNOf := NewScriptNOf(2, scriptSig, scriptSig)
933+
assert.Equal(t, 3, scriptNOf.Type())
934+
cbor = scriptNOf.CBOR()
935+
assert.NotEmpty(t, cbor)
936+
937+
// Test script before
938+
scriptBefore := NewScriptBefore(123456789)
939+
assert.Equal(t, 4, scriptBefore.Type())
940+
cbor = scriptBefore.CBOR()
941+
assert.NotEmpty(t, cbor)
942+
943+
// Test script after
944+
scriptAfter := NewScriptAfter(123456789)
945+
assert.Equal(t, 5, scriptAfter.Type())
946+
cbor = scriptAfter.CBOR()
947+
assert.NotEmpty(t, cbor)
948+
}
949+
950+
func TestGetScriptHash(t *testing.T) {
951+
script := NewScriptSig([]byte{0x01, 0x02, 0x03})
952+
hash := GetScriptHash(script)
953+
assert.Len(t, hash, 28)
954+
}
955+
956+
func TestGetScriptAddress(t *testing.T) {
957+
script := NewScriptSig([]byte{0x01, 0x02, 0x03})
958+
addr, err := GetScriptAddress(script, "mainnet")
959+
assert.NoError(t, err)
960+
assert.NotEmpty(t, addr)
961+
assert.True(t, strings.HasPrefix(addr, "addr1"))
962+
963+
// Test invalid network
964+
_, err = GetScriptAddress(script, "invalid")
965+
assert.Error(t, err)
966+
assert.Equal(t, ErrInvalidNetwork, err)
967+
}
968+
969+
func TestMultiSigKeyDerivation(t *testing.T) {
970+
mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
971+
rootKey, err := GetRootKeyFromMnemonic(mnemonic, "")
972+
assert.NoError(t, err)
973+
974+
accountKey, err := GetMultiSigAccountKey(rootKey, 0)
975+
assert.NoError(t, err)
976+
977+
paymentKey, err := GetMultiSigPaymentKey(accountKey, 0)
978+
assert.NoError(t, err)
979+
980+
vkey, err := GetMultiSigPaymentVKey(paymentKey)
981+
assert.NoError(t, err)
982+
assert.Equal(t, "PaymentVerificationKeyShelley_ed25519", vkey.Type)
983+
984+
skey, err := GetMultiSigPaymentSKey(paymentKey)
985+
assert.NoError(t, err)
986+
assert.Equal(t, "PaymentSigningKeyShelley_ed25519", skey.Type)
987+
988+
stakeKey, err := GetMultiSigStakeKey(accountKey, 0)
989+
assert.NoError(t, err)
990+
991+
stakeVKey, err := GetMultiSigStakeVKey(stakeKey)
992+
assert.NoError(t, err)
993+
assert.Equal(t, "StakeVerificationKeyShelley_ed25519", stakeVKey.Type)
994+
995+
stakeSKey, err := GetMultiSigStakeSKey(stakeKey)
996+
assert.NoError(t, err)
997+
assert.Equal(t, "StakeSigningKeyShelley_ed25519", stakeSKey.Type)
998+
}
999+
1000+
func TestValidateScript(t *testing.T) {
1001+
// Test ScriptSig validation (allows empty signatures for basic validation)
1002+
scriptSig := NewScriptSig([]byte{0x01, 0x02, 0x03})
1003+
assert.True(t, ValidateScript(scriptSig, nil, 1000))
1004+
1005+
// Test ScriptAll validation (requires all sub-scripts satisfied)
1006+
// Since ScriptSig allows empty signatures, ScriptAll should succeed
1007+
scriptAll := NewScriptAll(
1008+
NewScriptSig([]byte{0x01}),
1009+
NewScriptSig([]byte{0x02}),
1010+
)
1011+
assert.True(t, ValidateScript(scriptAll, nil, 1000))
1012+
1013+
// Test ScriptAny validation (requires any sub-script satisfied)
1014+
// Since ScriptSig allows empty signatures, ScriptAny should succeed
1015+
scriptAny := NewScriptAny(
1016+
NewScriptSig([]byte{0x01}),
1017+
NewScriptSig([]byte{0x02}),
1018+
)
1019+
assert.True(t, ValidateScript(scriptAny, nil, 1000))
1020+
1021+
// Test ScriptNOf validation (2-of-3)
1022+
// Since ScriptSig allows empty signatures, ScriptNOf should succeed
1023+
scriptNOf := NewScriptNOf(2,
1024+
NewScriptSig([]byte{0x01}),
1025+
NewScriptSig([]byte{0x02}),
1026+
NewScriptSig([]byte{0x03}),
1027+
)
1028+
assert.True(t, ValidateScript(scriptNOf, nil, 1000))
1029+
1030+
// Test ScriptBefore validation
1031+
scriptBefore := NewScriptBefore(2000)
1032+
assert.True(t, ValidateScript(scriptBefore, nil, 1000)) // Slot 1000 < 2000
1033+
assert.False(t, ValidateScript(scriptBefore, nil, 3000)) // Slot 3000 > 2000
1034+
1035+
// Test ScriptAfter validation
1036+
scriptAfter := NewScriptAfter(1000)
1037+
assert.True(t, ValidateScript(scriptAfter, nil, 2000)) // Slot 2000 > 1000
1038+
assert.False(t, ValidateScript(scriptAfter, nil, 500)) // Slot 500 < 1000
1039+
}
1040+
1041+
func TestMultiSigScriptGeneration(t *testing.T) {
1042+
keyHash1 := []byte{0x01, 0x02, 0x03}
1043+
keyHash2 := []byte{0x04, 0x05, 0x06}
1044+
keyHash3 := []byte{0x07, 0x08, 0x09}
1045+
1046+
// Test NewMultiSigScript (2-of-3)
1047+
script2of3 := NewMultiSigScript(2, keyHash1, keyHash2, keyHash3)
1048+
assert.Equal(t, 3, script2of3.Type()) // NOf type
1049+
assert.Equal(t, 2, script2of3.N)
1050+
assert.Len(t, script2of3.Scripts, 3)
1051+
1052+
// Test NewAllMultiSigScript
1053+
scriptAll := NewAllMultiSigScript(keyHash1, keyHash2)
1054+
assert.Equal(t, 1, scriptAll.Type()) // All type
1055+
assert.Len(t, scriptAll.Scripts, 2)
1056+
1057+
// Test NewAnyMultiSigScript
1058+
scriptAny := NewAnyMultiSigScript(keyHash1, keyHash2, keyHash3)
1059+
assert.Equal(t, 2, scriptAny.Type()) // Any type
1060+
assert.Len(t, scriptAny.Scripts, 3)
1061+
1062+
// Test NewTimelockedScript (before)
1063+
timelockedBefore := NewTimelockedScript(1000, true, scriptAll)
1064+
assert.Equal(t, 1, timelockedBefore.Type()) // All type (wrapping)
1065+
allScript := timelockedBefore.(*ScriptAll)
1066+
assert.Len(t, allScript.Scripts, 2) // Before script + original script
1067+
1068+
// Test NewTimelockedScript (after)
1069+
timelockedAfter := NewTimelockedScript(2000, false, scriptAny)
1070+
assert.Equal(t, 1, timelockedAfter.Type()) // All type (wrapping)
1071+
allScriptAfter := timelockedAfter.(*ScriptAll)
1072+
assert.Len(t, allScriptAfter.Scripts, 2) // After script + original script
1073+
}
1074+
1075+
func TestMultiSigScriptFromKeys(t *testing.T) {
1076+
// Create some dummy Ed25519 public keys
1077+
pubKey1, _, _ := ed25519.GenerateKey(nil)
1078+
pubKey2, _, _ := ed25519.GenerateKey(nil)
1079+
pubKey3, _, _ := ed25519.GenerateKey(nil)
1080+
1081+
// Test creating 2-of-3 script from public keys
1082+
script := NewMultiSigScriptFromKeys(2, pubKey1, pubKey2, pubKey3)
1083+
assert.Equal(t, 3, script.Type()) // NOf type
1084+
assert.Equal(t, 2, script.N)
1085+
assert.Len(t, script.Scripts, 3)
1086+
1087+
// Verify the script can be validated
1088+
assert.True(t, ValidateScript(script, nil, 1000))
1089+
}
1090+
1091+
func TestScriptGenerationEdgeCases(t *testing.T) {
1092+
keyHash := []byte{0x01, 0x02, 0x03}
1093+
1094+
// Test invalid parameters (should panic)
1095+
assert.Panics(t, func() { NewMultiSigScript(0, keyHash) }) // required < 1
1096+
assert.Panics(
1097+
t,
1098+
func() { NewMultiSigScript(2, keyHash) },
1099+
) // required > len(keyHashes)
1100+
assert.Panics(t, func() { NewMultiSigScript(1) }) // no key hashes
1101+
assert.Panics(t, func() { NewAllMultiSigScript() }) // no key hashes
1102+
assert.Panics(t, func() { NewAnyMultiSigScript() }) // no key hashes
1103+
}

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)