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 <charon@lethean.io>
This commit is contained in:
Claude 2026-03-16 20:28:55 +00:00
parent 1ca75f9e3f
commit 14a2da9396
No known key found for this signature in database
GPG key ID: AF404715446AEB41
2 changed files with 157 additions and 0 deletions

View file

@ -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

View file

@ -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())
}
}