Skip to content
Open
28 changes: 28 additions & 0 deletions plugin/dapp/lightclient/rpc/lightclient/neutrino/bitcoin.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
ltypes "github.com/33cn/plugin/plugin/dapp/lightclient/lighttypes"
rtypes "github.com/33cn/plugin/plugin/dapp/rgbx/types"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/walletdb"
)
Expand Down Expand Up @@ -579,3 +580,30 @@ func (n *neutrinoClient) processWithdrawConfirm(confirm *confirmWithdraw) bool {
return true

}

// buildNativeConfirmBtcTxProof 构造原生资产 Confirm 交易的 BTC Merkle 证明
func (n *neutrinoClient) buildNativeConfirmBtcTxProof(spendingTx *wire.MsgTx, blockHash chainhash.Hash, blockHeight uint32) (*rtypes.BtcTxProof, error) {
// 创建 btcPendingTx 包装,复用 buildTxExistenceProof
pending := &btcPendingTx{
tx: spendingTx,
blockHash: blockHash,
blockHeight: int32(blockHeight),
txHash: spendingTx.TxHash(),
}
spv, err := n.bw.buildTxExistenceProof(pending)
if err != nil {
return nil, err
}
// 序列化交易 txData
buf := bytes.NewBuffer(make([]byte, 0, spendingTx.SerializeSizeStripped()))
if err = spendingTx.SerializeNoWitness(buf); err != nil {
return nil, err
}
return &rtypes.BtcTxProof{
TxData: buf.Bytes(),
BlockHash: blockHash.String(),
BlockHeight: uint64(blockHeight),
TxIndex: spv.GetTxIndex(),
MerkleProof: spv.GetBranchProof(),
}, nil
}
44 changes: 36 additions & 8 deletions plugin/dapp/lightclient/rpc/lightclient/neutrino/rgbx.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type utxoSpendInfo struct {
pendingTxHash string
spendingTxHash string
spendingTx *wire.MsgTx
blockHash chainhash.Hash
timeout bool
}

Expand Down Expand Up @@ -207,12 +208,23 @@ func (r *rgbx) rescanUtxo(info *utxoRescanInfo) (success bool) {
return false
}

// 获取 spending tx 所在区块的 block hash
var blockHash chainhash.Hash
header, err := r.client.neutrinoCS.BlockHeaders.FetchHeaderByHeight(spendReport.SpendingTxHeight)
if err != nil {
log.Error("rescanUtxo FetchHeaderByHeight", "pendingTxHash", info.pendingTxHash,
"height", spendReport.SpendingTxHeight, "err", err)
return false
}
blockHash = header.BlockHash()

spendInfo := &utxoSpendInfo{
spendingTxHash: spendReport.SpendingTx.TxHash().String(),
pendingTxHash: info.pendingTxHash,
spendingTx: spendReport.SpendingTx,
spendingHeight: spendReport.SpendingTxHeight,
spendingInputIndex: spendReport.SpendingInputIndex,
blockHash: blockHash,
}

r.commitChan <- spendInfo
Expand Down Expand Up @@ -285,21 +297,37 @@ func (r *rgbx) createConfirmPayload(info *utxoSpendInfo, pendTx *rtypes.PendingT
SpendingInputIdx: info.spendingInputIndex,
OpRetOutputIdx: -1,
}
expectedCommitment, err := txscript.NullDataScript(pendTx.GetTxHash())
if err != nil {
log.Error("createConfirmPayload NullDataScript", "pendingTxHash", info.pendingTxHash, "err", err)
return nil, err
}
firstOpRetIdx := int32(-1)
for idx, out := range info.spendingTx.TxOut {
if len(out.PkScript) > 0 && out.PkScript[0] == txscript.OP_RETURN {
proof.OpRetOutputIdx = int32(idx)
proof.OpRetOutputPkScript = out.PkScript
if firstOpRetIdx < 0 {
firstOpRetIdx = int32(idx)
}
if bytes.Equal(out.PkScript, expectedCommitment) {
proof.OpRetOutputIdx = int32(idx)
break
}
}
}
buf := bytes.NewBuffer(make([]byte, 0, info.spendingTx.SerializeSizeStripped()))
err := info.spendingTx.SerializeNoWitness(buf)
if err != nil {
log.Error("createConfirmPayload", "pendingTxHash", info.pendingTxHash, "serialize spending tx err", err)
return nil, err
if proof.OpRetOutputIdx < 0 {
proof.OpRetOutputIdx = firstOpRetIdx
}
proof.SpendingTx = buf.Bytes()
confirm.UtxoProof = proof

if r.client != nil {
btcProof, err := r.client.buildNativeConfirmBtcTxProof(info.spendingTx, info.blockHash, info.spendingHeight)
if err != nil {
log.Error("createConfirmPayload buildNativeConfirmBtcTxProof", "pendingTxHash", info.pendingTxHash, "err", err)
return nil, err
}
confirm.BtcTxProof = btcProof
}

return confirm, nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func Test_rgbx_createConfirmPayload_withSpendingTxAndOpReturn(t *testing.T) {
pendingTxHash: "abc",
spendingInputIndex: 0,
spendingTx: spendTx,
blockHash: chainhash.Hash{0x01},
}
pendTx := &rtypes.PendingTx{
ActionType: rtypes.TyMintAction,
Expand All @@ -87,11 +88,40 @@ func Test_rgbx_createConfirmPayload_withSpendingTxAndOpReturn(t *testing.T) {
require.NotNil(t, confirm.UtxoProof)
require.Equal(t, uint32(0), confirm.UtxoProof.SpendingInputIdx)
require.GreaterOrEqual(t, confirm.UtxoProof.OpRetOutputIdx, int32(0))
require.Equal(t, byte(txscript.OP_RETURN), confirm.UtxoProof.OpRetOutputPkScript[0])
require.Nil(t, confirm.BtcTxProof)
}

func Test_rgbx_createConfirmPayload_prefersMatchingCommitmentOpReturn(t *testing.T) {
r := newRGBX()
r.pendingCache.addTx("abc", &rtypes.PendingTx{TxBlockHeight: 10})

spendTx := wire.NewMsgTx(wire.TxVersion)
spendTx.AddTxIn(wire.NewTxIn(&wire.OutPoint{}, nil, nil))
wrongOpRet, err := txscript.NullDataScript([]byte("rgbx:wrong"))
require.NoError(t, err)
rightCommitment, err := txscript.NullDataScript([]byte{0x11, 0x22, 0x33})
require.NoError(t, err)
spendTx.AddTxOut(wire.NewTxOut(0, wrongOpRet))
spendTx.AddTxOut(wire.NewTxOut(0, rightCommitment))
spendTx.AddTxOut(wire.NewTxOut(1000, []byte{txscript.OP_0, 0x14}))

info := &utxoSpendInfo{
pendingTxHash: "abc",
spendingInputIndex: 0,
spendingTx: spendTx,
blockHash: chainhash.Hash{0x01},
}
pendTx := &rtypes.PendingTx{
ActionType: rtypes.TyMintAction,
TxBlockHeight: 5,
TxIndex: 1,
TxHash: []byte{0x11, 0x22, 0x33},
}

var back wire.MsgTx
require.NoError(t, back.DeserializeNoWitness(bytes.NewReader(confirm.UtxoProof.SpendingTx)))
require.Equal(t, spendTx.TxHash().String(), back.TxHash().String())
confirm, err := r.createConfirmPayload(info, pendTx)
require.NoError(t, err)
require.NotNil(t, confirm.UtxoProof)
require.Equal(t, int32(1), confirm.UtxoProof.OpRetOutputIdx)
}

func Test_estimateBtcFee(t *testing.T) {
Expand Down
135 changes: 134 additions & 1 deletion plugin/dapp/rgbx/cmd/ci/docker-compose.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ ACTION="run"
PROJECT=""
if [ "$#" -gt 0 ]; then
case "${1}" in
run | up | down | init | config | test)
run | up | down | init | config | test | native | all)
ACTION="${1}"
PROJECT="${2:-build}"
;;
Expand Down Expand Up @@ -698,6 +698,110 @@ function scenario_para_health() {
${PARA4_CLI} net is_sync >/dev/null
}

function scenario_native_asset_mint() {
log_step "scenario: native asset mint with btc spending confirm"

# 1. Get a mature coinbase UTXO
local utxo
utxo=$(build_mature_coinbase_utxo)
assert_non_empty "${utxo}" "native mint funding utxo empty"

local utxo_txid
local utxo_vout
local utxo_amount
local utxo_pkscript
utxo_txid=$(echo "${utxo}" | cut -d: -f1)
utxo_vout=$(echo "${utxo}" | cut -d: -f2)
utxo_amount=$(echo "${utxo}" | cut -d: -f3)
utxo_pkscript=$(echo "${utxo}" | cut -d: -f4)
assert_non_empty "${utxo_txid}" "native mint utxo txid empty"
assert_non_empty "${utxo_pkscript}" "native mint utxo pkscript empty"

# genesis_out format for mint: hash:index:pkScript
local genesis_out="${utxo_txid}:${utxo_vout}:${utxo_pkscript}"

# Verify chain33 is producing blocks before mint
local pre_height
pre_height=$(${MAIN_CLI} block last_header | jq '.height')
log_step "chain33 height before mint: ${pre_height}"

# 2. Mint a native asset on chain33 with the genesis UTXO
local mint_hash
local mint_rc
local mint_stdout
mint_stdout=$(${MAIN_CLI} send rgbx mint -s NATIVE1 -a 10000 -o "${genesis_out}" -m "6e6174697665316d657461" -k "${GENESIS_KEY}" 2>/tmp/mint_stderr.$$)
mint_rc=$?
mint_hash="${mint_stdout}"
if [ ${mint_rc} -ne 0 ] || [ -z "${mint_hash}" ] || [ ${#mint_hash} -lt 64 ]; then
log_step "mint send stderr: $(head -5 /tmp/mint_stderr.$$ 2>/dev/null)"
rm -f /tmp/mint_stderr.$$
fail "native mint send failed, rc=${mint_rc}, hash=${mint_hash}"
fi
rm -f /tmp/mint_stderr.$$

# Wait for mint tx to be confirmed on chain33
# send command already waits for the tx to be committed in a block
local post_height
post_height=$(${MAIN_CLI} block last_header | jq '.height')
log_step "mint tx hash: ${mint_hash}, height after mint: ${post_height}"

# 3. Get the full mint tx hash from chain33 for OP_RETURN commitment
# The send output gives us the chain33 tx hash hex (may contain 0x prefix)
local mint_tx_hash_hex="${mint_hash}"
mint_tx_hash_hex="${mint_tx_hash_hex#0x}"

# 4. Construct, sign, and broadcast BTC spending transaction using btcMintSpend
local fee=1000
local spend_amount=$((utxo_amount - fee))
if [ "${spend_amount}" -le 0 ]; then
fail "native mint insufficient utxo amount=${utxo_amount}, fee=${fee}"
fi

local spend_txid
spend_txid=$(compose_cmd exec -T main /root/chain33-cli rgbx btcMintSpend \
--net "${BTC_NETWORK}" \
--rpcHost "${BTC_RPC_ADDR}" \
--rpcUser "${BTCD_RPC_USER}" \
--rpcPass "${BTCD_RPC_PASS}" \
--disableTLS=false \
--rpcCertFile "${BTCD_RPC_CERT_IN_CONTAINER}" \
--wif "${BTC_FUNDING_WIF}" \
--utxo "${utxo}" \
--destAddress "${BTCD_MINING_ADDR}" \
--opReturnData "${mint_tx_hash_hex}" \
--fee "${fee}")
assert_non_empty "${spend_txid}" "native mint btcMintSpend failed"

# Mine BTC blocks for confirmations (neutrino uses blockConfirmations=1)
mine_btcd_blocks 2

# 5. Wait for the neutrino service to detect the UTXO spend and submit a confirm tx
log_step "wait for neutrino to confirm native asset mint (NATIVE1)"
local retries=60
local i
for ((i = 0; i < retries; i++)); do
set +e
local asset_info
asset_info=$(${MAIN_CLI} rgbx getAsset -s NATIVE1 2>/dev/null)
local rc=$?
set -e
if [ "${rc}" -eq 0 ]; then
local symbol
symbol=$(echo "${asset_info}" | jq -r '.symbol // empty')
if [ "${symbol}" = "NATIVE1" ]; then
local total_amount
total_amount=$(echo "${asset_info}" | jq -r '.totalAmount // 0')
log_step "native asset NATIVE1 created, totalAmount=${total_amount}"
return 0
fi
fi
mine_btcd_blocks 1
sleep 2
done

fail "native asset NATIVE1 not created after timeout"
}

function ensure_btcd_network_consistency() {
if [ "${BTC_NETWORK}" != "regtest" ]; then
fail "unsupported BTC_NETWORK=${BTC_NETWORK}; this CI uses btcd --regtest only"
Expand All @@ -715,9 +819,28 @@ function run_tests() {
scenario_user_deposit_via_btc_tx
scenario_user_transfer_crosschain_asset
scenario_user_withdraw_auto_confirm
# Run native asset test before restart so chain33 consensus is stable
scenario_native_asset_mint
scenario_restart_recovery
}

function run_native_tests() {
ensure_btcd_network_consistency
prepare_btcd_mining_identity
wait_btcd_ready
scenario_para_health
setup_para_nodegroup_on_main
wait_auto_dkg_commit

# Mine BTC blocks so build_mature_coinbase_utxo can find a mature coinbase
mine_btcd_blocks 101
scenario_native_asset_mint
}

function run_all_tests_wrapper() {
run_tests
}

function print_logs_hint() {
log_step "collect logs with: ${COMPOSE_BIN} logs --tail=200"
log_step "neutrino peer endpoint: ${BTC_P2P_ADDR}"
Expand Down Expand Up @@ -751,6 +874,16 @@ case "${ACTION}" in
run)
do_run_all
;;
native)
do_up_only
run_native_tests
print_logs_hint
;;
all)
do_up_only
run_all_tests_wrapper
print_logs_hint
;;
up)
do_up_only
;;
Expand Down
9 changes: 9 additions & 0 deletions plugin/dapp/rgbx/cmd/ci/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
services:
main:
platform: linux/amd64
build:
context: .
environment:
Expand All @@ -11,6 +12,7 @@ services:
- ./btcd-data:/btcd:ro

para1:
platform: linux/amd64
build:
context: .
environment:
Expand All @@ -24,36 +26,43 @@ services:
- ./btcd-data:/btcd:ro

para2:
platform: linux/amd64
build:
context: .
environment:
ChainConfig: "chain33.para2.toml"
depends_on:
- main
restart: unless-stopped
volumes:
- ./btcd-data:/btcd:ro

para3:
platform: linux/amd64
build:
context: .
environment:
ChainConfig: "chain33.para3.toml"
depends_on:
- main
restart: unless-stopped
volumes:
- ./btcd-data:/btcd:ro

para4:
platform: linux/amd64
build:
context: .
environment:
ChainConfig: "chain33.para4.toml"
depends_on:
- main
restart: unless-stopped
volumes:
- ./btcd-data:/btcd:ro

btcd:
platform: linux/amd64
# RPC TLS cert auto-gen uses os.Hostname(); must match Docker DNS name "btcd"
# so para/lightclient can dial btcd:18443 without SAN mismatch.
hostname: btcd
Expand Down
Loading
Loading