From 14a2da93969e1ea161d59461e057795d2263a824 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 20:28:55 +0000 Subject: [PATCH] feat(wire): encode/decode TxInputHTLC and TxInputMultisig Adds wire serialisation for HF1 HTLC (tag 0x22) and multisig (tag 0x02) input types. Co-Authored-By: Charon --- wire/transaction.go | 34 +++++++++++ wire/transaction_test.go | 123 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/wire/transaction.go b/wire/transaction.go index 36bf0fd..e53daed 100644 --- a/wire/transaction.go +++ b/wire/transaction.go @@ -162,6 +162,21 @@ func encodeInputs(enc *Encoder, vin []types.TxInput) { encodeKeyOffsets(enc, v.KeyOffsets) enc.WriteBlob32((*[32]byte)(&v.KeyImage)) enc.WriteBytes(v.EtcDetails) + case types.TxInputHTLC: + // Wire order: hltc_origin (string) BEFORE parent fields (C++ quirk). + enc.WriteVarint(uint64(len(v.HTLCOrigin))) + if len(v.HTLCOrigin) > 0 { + enc.WriteBytes([]byte(v.HTLCOrigin)) + } + enc.WriteVarint(v.Amount) + encodeKeyOffsets(enc, v.KeyOffsets) + enc.WriteBlob32((*[32]byte)(&v.KeyImage)) + enc.WriteBytes(v.EtcDetails) + case types.TxInputMultisig: + enc.WriteVarint(v.Amount) + enc.WriteBlob32((*[32]byte)(&v.MultisigOutID)) + enc.WriteVarint(v.SigsCount) + enc.WriteBytes(v.EtcDetails) } } } @@ -193,6 +208,25 @@ func decodeInputs(dec *Decoder) []types.TxInput { dec.ReadBlob32((*[32]byte)(&in.KeyImage)) in.EtcDetails = decodeRawVariantVector(dec) vin = append(vin, in) + case types.InputTypeHTLC: + var in types.TxInputHTLC + // Wire order: hltc_origin (string) BEFORE parent fields. + originLen := dec.ReadVarint() + if originLen > 0 && dec.Err() == nil { + in.HTLCOrigin = string(dec.ReadBytes(int(originLen))) + } + in.Amount = dec.ReadVarint() + in.KeyOffsets = decodeKeyOffsets(dec) + dec.ReadBlob32((*[32]byte)(&in.KeyImage)) + in.EtcDetails = decodeRawVariantVector(dec) + vin = append(vin, in) + case types.InputTypeMultisig: + var in types.TxInputMultisig + in.Amount = dec.ReadVarint() + dec.ReadBlob32((*[32]byte)(&in.MultisigOutID)) + in.SigsCount = dec.ReadVarint() + in.EtcDetails = decodeRawVariantVector(dec) + vin = append(vin, in) default: dec.err = fmt.Errorf("wire: unsupported input tag 0x%02x", tag) return vin diff --git a/wire/transaction_test.go b/wire/transaction_test.go index 562bb35..602a34a 100644 --- a/wire/transaction_test.go +++ b/wire/transaction_test.go @@ -467,3 +467,126 @@ func TestRefByIDRoundTrip_Good(t *testing.T) { t.Errorf("ref N: got %d, want 3", ref.N) } } + +func TestHTLCInputRoundTrip_Good(t *testing.T) { + tx := types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{ + types.TxInputHTLC{ + HTLCOrigin: "test_origin", + Amount: 42000, + KeyOffsets: []types.TxOutRef{ + {Tag: types.RefTypeGlobalIndex, GlobalIndex: 100}, + }, + KeyImage: types.KeyImage{0xAA}, + EtcDetails: EncodeVarint(0), + }, + }, + Vout: []types.TxOutput{types.TxOutputBare{ + Amount: 41000, + Target: types.TxOutToKey{Key: types.PublicKey{0x01}}, + }}, + Extra: EncodeVarint(0), + } + + var buf bytes.Buffer + enc := NewEncoder(&buf) + EncodeTransactionPrefix(enc, &tx) + if enc.Err() != nil { + t.Fatalf("encode error: %v", enc.Err()) + } + + dec := NewDecoder(bytes.NewReader(buf.Bytes())) + got := DecodeTransactionPrefix(dec) + if dec.Err() != nil { + t.Fatalf("decode error: %v", dec.Err()) + } + + if len(got.Vin) != 1 { + t.Fatalf("vin count: got %d, want 1", len(got.Vin)) + } + htlc, ok := got.Vin[0].(types.TxInputHTLC) + if !ok { + t.Fatalf("vin[0] type: got %T, want TxInputHTLC", got.Vin[0]) + } + if htlc.HTLCOrigin != "test_origin" { + t.Errorf("HTLCOrigin: got %q, want %q", htlc.HTLCOrigin, "test_origin") + } + if htlc.Amount != 42000 { + t.Errorf("Amount: got %d, want 42000", htlc.Amount) + } + if htlc.KeyImage[0] != 0xAA { + t.Errorf("KeyImage[0]: got 0x%02x, want 0xAA", htlc.KeyImage[0]) + } + + // Byte-level round-trip. + var rtBuf bytes.Buffer + enc2 := NewEncoder(&rtBuf) + EncodeTransactionPrefix(enc2, &got) + if enc2.Err() != nil { + t.Fatalf("re-encode error: %v", enc2.Err()) + } + if !bytes.Equal(rtBuf.Bytes(), buf.Bytes()) { + t.Errorf("round-trip mismatch:\n got: %x\n want: %x", rtBuf.Bytes(), buf.Bytes()) + } +} + +func TestMultisigInputRoundTrip_Good(t *testing.T) { + tx := types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{ + types.TxInputMultisig{ + Amount: 50000, + MultisigOutID: types.Hash{0xBB}, + SigsCount: 3, + EtcDetails: EncodeVarint(0), + }, + }, + Vout: []types.TxOutput{types.TxOutputBare{ + Amount: 49000, + Target: types.TxOutToKey{Key: types.PublicKey{0x02}}, + }}, + Extra: EncodeVarint(0), + } + + var buf bytes.Buffer + enc := NewEncoder(&buf) + EncodeTransactionPrefix(enc, &tx) + if enc.Err() != nil { + t.Fatalf("encode error: %v", enc.Err()) + } + + dec := NewDecoder(bytes.NewReader(buf.Bytes())) + got := DecodeTransactionPrefix(dec) + if dec.Err() != nil { + t.Fatalf("decode error: %v", dec.Err()) + } + + if len(got.Vin) != 1 { + t.Fatalf("vin count: got %d, want 1", len(got.Vin)) + } + msig, ok := got.Vin[0].(types.TxInputMultisig) + if !ok { + t.Fatalf("vin[0] type: got %T, want TxInputMultisig", got.Vin[0]) + } + if msig.Amount != 50000 { + t.Errorf("Amount: got %d, want 50000", msig.Amount) + } + if msig.MultisigOutID[0] != 0xBB { + t.Errorf("MultisigOutID[0]: got 0x%02x, want 0xBB", msig.MultisigOutID[0]) + } + if msig.SigsCount != 3 { + t.Errorf("SigsCount: got %d, want 3", msig.SigsCount) + } + + // Byte-level round-trip. + var rtBuf bytes.Buffer + enc2 := NewEncoder(&rtBuf) + EncodeTransactionPrefix(enc2, &got) + if enc2.Err() != nil { + t.Fatalf("re-encode error: %v", enc2.Err()) + } + if !bytes.Equal(rtBuf.Bytes(), buf.Bytes()) { + t.Errorf("round-trip mismatch:\n got: %x\n want: %x", rtBuf.Bytes(), buf.Bytes()) + } +}