Last active
July 2, 2025 03:45
-
-
Save lukechampine/38c9c45cb327993d962219e34e50a6c7 to your computer and use it in GitHub Desktop.
og-sweep: Sweep v033x and siag keys
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"flag" | |
"fmt" | |
"log" | |
"os" | |
"go.sia.tech/core/types" | |
"go.sia.tech/walletd/v2/api" | |
) | |
func readFile(path string, v types.DecoderFrom) { | |
data, err := os.ReadFile(path) | |
if err != nil { | |
log.Fatalln("Error reading file:", err) | |
} | |
d := types.NewBufDecoder(data) | |
v.DecodeFrom(d) | |
if d.Err() != nil { | |
log.Fatalln("Failed to decode file:", d.Err()) | |
} | |
} | |
type savedKey033x struct { | |
SecretKey types.PrivateKey | |
UnlockConditions types.UnlockConditions | |
Visible bool | |
} | |
func (sk *savedKey033x) DecodeFrom(d *types.Decoder) { | |
var key [64]byte | |
d.Read(key[:]) | |
sk.SecretKey = types.PrivateKey(key[:]) | |
sk.UnlockConditions.DecodeFrom(d) | |
sk.Visible = d.ReadBool() | |
} | |
func extract033x(path string) (keys []savedKey033x) { | |
readFile(path, types.DecoderFunc(func(d *types.Decoder) { | |
types.DecodeSlice(d, &keys) | |
})) | |
return | |
} | |
type siagKey struct { | |
Header string | |
Version string | |
Index int | |
SecretKey types.PrivateKey | |
UnlockConditions types.UnlockConditions | |
} | |
func (sk *siagKey) DecodeFrom(d *types.Decoder) { | |
sk.Header = d.ReadString() | |
sk.Version = d.ReadString() | |
sk.Index = int(d.ReadUint64()) | |
sk.SecretKey = make(types.PrivateKey, 64) | |
d.Read(sk.SecretKey[:]) | |
sk.UnlockConditions.DecodeFrom(d) | |
} | |
func extractSiag(path string) (key siagKey) { | |
readFile(path, &key) | |
return | |
} | |
type signer struct { | |
UnlockConditions types.UnlockConditions | |
Keys []types.PrivateKey | |
} | |
func utxos(addr types.Address) (scs []types.SiacoinElement, sfs []types.SiafundElement, basis types.ChainIndex, err error) { | |
c := api.NewClient("https://api.siascan.com/wallet", "") | |
again: | |
sces, scBasis, err := c.AddressSiacoinOutputs(addr, false, 0, 10000) | |
if err != nil { | |
return nil, nil, types.ChainIndex{}, fmt.Errorf("failed to get siacoin elements for address %s: %w", addr, err) | |
} | |
sfes, sfBasis, err := c.AddressSiafundOutputs(addr, false, 0, 10000) | |
if err != nil { | |
return nil, nil, types.ChainIndex{}, fmt.Errorf("failed to get siafund elements for address %s: %w", addr, err) | |
} | |
if scBasis != sfBasis { | |
goto again | |
} | |
for _, sce := range sces { | |
scs = append(scs, sce.SiacoinElement) | |
} | |
for _, sfe := range sfes { | |
sfs = append(sfs, sfe.SiafundElement) | |
} | |
basis = scBasis | |
return | |
} | |
func main() { | |
log.SetFlags(log.Lshortfile) | |
v033x := flag.Bool("v033x", false, "decode a v033x wallet key") | |
siag := flag.Bool("siag", false, "decode a set of .siakey files") | |
flag.Parse() | |
var signers []signer | |
if *v033x { | |
keys := extract033x(flag.Arg(0)) | |
if len(keys) == 0 { | |
fmt.Println("No keys found") | |
return | |
} | |
for _, key := range keys { | |
signers = append(signers, signer{ | |
UnlockConditions: key.UnlockConditions, | |
Keys: []types.PrivateKey{key.SecretKey}, | |
}) | |
} | |
} else if *siag { | |
if len(flag.Args()) == 0 { | |
fmt.Println("No .siakey files provided") | |
return | |
} | |
keys := make([]types.PrivateKey, 0, len(flag.Args())) | |
var uc types.UnlockConditions | |
for i, arg := range flag.Args() { | |
key := extractSiag(arg) | |
if i == 0 { | |
uc = key.UnlockConditions | |
} else { | |
if key.UnlockConditions.UnlockHash() != uc.UnlockHash() { | |
fmt.Println("All keys must have the same unlock conditions") | |
return | |
} | |
} | |
keys = append(keys, key.SecretKey) | |
} | |
signers = append(signers, signer{ | |
UnlockConditions: uc, | |
Keys: keys, | |
}) | |
} else { | |
fmt.Println("Usage: og-sweep [-v033x] [-siag] <files>") | |
fmt.Println(" -v033x: decode a v033x wallet key") | |
fmt.Println(" -siag: decode a set of .siakey files") | |
return | |
} | |
retry: | |
var setBasis types.ChainIndex | |
var txn types.V2Transaction | |
var totalSC types.Currency | |
var totalSF uint64 | |
for i, sp := range signers { | |
fmt.Print("\rChecking address: ", sp.UnlockConditions.UnlockHash(), "... ") | |
sces, sfes, basis, err := utxos(sp.UnlockConditions.UnlockHash()) | |
if err != nil { | |
fmt.Println() | |
log.Fatal(err) | |
} else if setBasis != (types.ChainIndex{}) && basis != setBasis { | |
goto retry | |
} else if len(sces) == 0 && len(sfes) == 0 { | |
if i == len(signers)-1 { | |
fmt.Println() | |
} | |
continue | |
} | |
setBasis = basis | |
var sc types.Currency | |
var sf uint64 | |
for i := range sces { | |
txn.SiacoinInputs = append(txn.SiacoinInputs, types.V2SiacoinInput{Parent: sces[i]}) | |
sc = sc.Add(sces[i].SiacoinOutput.Value) | |
} | |
for i := range sfes { | |
txn.SiafundInputs = append(txn.SiafundInputs, types.V2SiafundInput{Parent: sfes[i]}) | |
sf += sfes[i].SiafundOutput.Value | |
} | |
fmt.Printf("Found %v and %v SF\n", sc, sf) | |
totalSC = totalSC.Add(sc) | |
totalSF += sf | |
} | |
if totalSC.IsZero() && totalSF == 0 { | |
fmt.Println("No SC or SF found.") | |
return | |
} | |
fmt.Printf("Total amount to sweep: %v and %v SF.\n", totalSC, totalSF) | |
again: | |
fmt.Print("Enter destination address: ") | |
var destStr string | |
fmt.Scanln(&destStr) | |
dest, err := types.ParseAddress(destStr) | |
if err != nil { | |
fmt.Println("Invalid address:", err) | |
goto again | |
} | |
if !totalSC.IsZero() { | |
txn.SiacoinOutputs = []types.SiacoinOutput{{Value: totalSC, Address: dest}} | |
} | |
if totalSF != 0 { | |
txn.SiafundOutputs = []types.SiafundOutput{{Value: totalSF, Address: dest}} | |
} | |
// sign transaction | |
c := api.NewClient("https://api.siascan.com/wallet", "") | |
cs, err := c.ConsensusTipState() | |
if err != nil { | |
log.Fatalln("Failed to get consensus tip state:", err) | |
} | |
sigHash := cs.InputSigHash(txn) | |
sps := make(map[types.Address]types.SatisfiedPolicy) | |
sign := func(addr types.Address) types.SatisfiedPolicy { | |
if sp, ok := sps[addr]; ok { | |
return sp | |
} | |
for _, s := range signers { | |
if s.UnlockConditions.UnlockHash() == addr { | |
sp := types.SatisfiedPolicy{Policy: types.SpendPolicy{Type: types.PolicyTypeUnlockConditions(s.UnlockConditions)}} | |
for _, key := range s.Keys[:s.UnlockConditions.SignaturesRequired] { | |
sp.Signatures = append(sp.Signatures, key.SignHash(sigHash)) | |
} | |
sps[addr] = sp | |
return sp | |
} | |
} | |
log.Fatalln("Failed to find signer for address:", addr) | |
panic("unreachable") | |
} | |
for i := range txn.SiacoinInputs { | |
txn.SiacoinInputs[i].SatisfiedPolicy = sign(txn.SiacoinInputs[i].Parent.SiacoinOutput.Address) | |
} | |
for i := range txn.SiafundInputs { | |
txn.SiafundInputs[i].SatisfiedPolicy = sign(txn.SiafundInputs[i].Parent.SiafundOutput.Address) | |
} | |
confirm: | |
fmt.Printf("Broadcast transaction %v? [y/n] ", txn.ID()) | |
var confirm string | |
fmt.Scanln(&confirm) | |
if confirm == "n" { | |
fmt.Println("Sweep cancelled.") | |
return | |
} else if confirm != "y" { | |
goto confirm | |
} | |
_, err = c.TxpoolBroadcast(setBasis, nil, []types.V2Transaction{txn}) | |
if err != nil { | |
log.Fatalln("Failed to broadcast transaction:", err) | |
} | |
fmt.Println("Transaction broadcast successfully.") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Binaries