Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cspell-custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ utest
UTXO
uusd
uvarint
vaas
varint
varints
vimdiff
Expand All @@ -275,5 +276,7 @@ XFER
xlayer
xpla
XPLA
XREG
XREL
XRPL
Zellic
3 changes: 3 additions & 0 deletions node/pkg/accountant/ntt_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ func nttGetEmitters(env common.Environment) (validEmitters, validEmitters, error
{chainId: vaa.ChainIDArbitrumSepolia, addr: "000000000000000000000000fA42603152E4f133F5F3DA610CDa91dF5821d8bc"},
{chainId: vaa.ChainIDBaseSepolia, addr: "000000000000000000000000149987472333cD48ac6D28293A338a1EEa6Be7EE"},
{chainId: vaa.ChainIDOptimismSepolia, addr: "000000000000000000000000eCF0496DE01e9Aa4ADB50ae56dB550f52003bdB7"},
// XRPL NTT custody emitters — one per (custody account, token), keyed on keccak256("ntt" + manager32 + sourceToken32). Log-only until the XREG registration pipeline is live.
Comment thread
johnsaigle marked this conversation as resolved.
{chainId: vaa.ChainIDXRPL, addr: "264a5b2d2d0d0722a05f3ebe0403aee658729456cb50b8c5d524036bd0c06aaa", logOnly: true}, // rpQdxE7TGjyhFWQeVrnxYo4j8dqwj9BTLH (XRP)
{chainId: vaa.ChainIDXRPL, addr: "31155b0ad6a8e59f4411d0276a915b90f36de48163a5ddc1886b59e4525cac24", logOnly: true}, // rN6CtQsYcAFBRciP9ktiPRv2R6FZ5sk5Fm (IOU "TS1")
}
arEmitterConfig = sdk.KnownTestnetAutomaticRelayerEmitters
} else {
Expand Down
38 changes: 38 additions & 0 deletions node/pkg/accountant/ntt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,44 @@ func TestNttParsePayloadNoTransferPrefix(t *testing.T) {
assert.False(t, nttIsPayloadNTT(payload))
}

// xrplXrpCustodyEmitter is the testnet NTT transceiver emitter for the XRP
// custody account rpQdxE7TGjyhFWQeVrnxYo4j8dqwj9BTLH —
// keccak256("ntt" + manager32 + zeros32). Registered in nttGetEmitters(TestNet).
const xrplXrpCustodyEmitter = "264a5b2d2d0d0722a05f3ebe0403aee658729456cb50b8c5d524036bd0c06aaa"

// TestNttXrplEmitterRegisteredTestnet verifies the XRPL custody emitters are
// present in the testnet config and that an XRPL NTT transfer from one is
// recognized as a (log-only) NTT message.
func TestNttXrplEmitterRegisteredTestnet(t *testing.T) {
directEmitters, _, err := nttGetEmitters(common.TestNet)
require.NoError(t, err)

emitterAddr, err := vaa.StringToAddress(xrplXrpCustodyEmitter)
require.NoError(t, err)

// Registered, and log-only (enforce flag false) for the initial rollout.
enforce, ok := directEmitters[emitterKey{emitterChainId: vaa.ChainIDXRPL, emitterAddr: emitterAddr}]
require.True(t, ok, "XRPL XRP custody emitter should be registered on testnet")
assert.False(t, enforce, "XRPL emitters should be log-only initially")

// An XRPL NTT transfer from this emitter is covered as a direct NTT message.
payload, err := hex.DecodeString(goodPayload)
require.NoError(t, err)

msg := &common.MessagePublication{
TxID: hashToTxID("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4063"),
Timestamp: time.Unix(int64(1654543099), 0),
Sequence: uint64(123456),
EmitterChain: vaa.ChainIDXRPL,
EmitterAddress: emitterAddr,
Payload: payload,
}

isNTT, enforceFlag := nttIsMsgDirectNTT(msg, directEmitters)
assert.True(t, isNTT, "XRPL NTT transfer should be recognized")
assert.False(t, enforceFlag, "should be log-only")
}

func TestNttParseMsgSuccess(t *testing.T) {
emitterAddr, err := vaa.StringToAddress("000000000000000000000000000000000000000000000000656e64706f696e74")
require.NoError(t, err)
Expand Down
83 changes: 79 additions & 4 deletions node/pkg/watchers/xrpl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,88 @@ uint32 nonce
[]byte payload
```

### Initialize Transceiver
### Registration (Initialize Transceiver & Peer Registration)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Because XRPL has no smart contracts, the [`WormholeTransceiverInfo`](https://github.com/wormhole-foundation/native-token-transfers/blob/main/docs/Transceiver.md#initialize-transceiver) (hub init) and
[`WormholeTransceiverRegistration`](https://github.com/wormhole-foundation/native-token-transfers/blob/main/docs/Transceiver.md#transceiver-peer-registration) (peer) messages cannot be emitted natively.
Instead, when a custody account is initialized (`initialize`) or a peer is
registered (`register_peer`), the Solana Sequencer emits the registration as an
`XrplRelease` (XREL) carrying the `XrplRegistration` (XREG) bytes in its first
memo. The delegated manager set signs and submits that XREL as a multisig Payment
to the Core Account (there is no standalone XREG message and the executor is not
involved). The watcher then re-keys and synthesizes the canonical transceiver
message under the correct NTT transceiver emitter.

> 🚧 TODO
Upon a Payment to the Core Account, the guardian watcher MUST

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this also must verify that the message came from the matching manager? though I'm confused about how an XREG message from the sequencer results in a payment from the manager without either manager changes in this PR or a generic payment message in the Sequencer PR. I would expect the latter - that the Sequencer issues a generic payment to the core bridge containing this registration. Maybe I'm missing where that occurs, but it looks like this is simply posted as a VAA payload. 😕

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed at sequencer level! XREG is wrapped in XREL which is signed by delegated manager and captured by watcher.


- Verify that the transaction is `validated`
- Verify that the transaction result is `tesSUCCESS`
- Verify that the transaction type is `Payment`
- Verify that the **first** memo (index 0) has a MemoFormat of
`application/x-ntt-registration`, hex-encoded
- Verify that the MemoData begins with the `XREG` prefix

The MemoData carries the raw `XREG` bytes:

```go
[4]byte prefix = "XREG"
uint8 kind // 0 = Hub, 1 = Peer
[20]byte manager // XRPL custody account id
[]byte token_id // wire form, per the sequencer token_id encoding (see below)
// Hub tail (kind=0):
uint8 token_decimals
// Peer tail (kind=1):
uint16 peer_chain // big-endian
[32]byte peer_address
```

The `token_id` uses the same encoding as the sequencer's XREL payload:

| Token Type | Byte 0 | Bytes 1+ | Total |
| ---------- | ------ | --------------------------- | -------- |
| XRP | 0x00 | (none) | 1 byte |
| IOU | 0x01 | currency (20) + issuer (20) | 41 bytes |
| MPT | 0x02 | mpt_issuance_id (24) | 25 bytes |

### Transceiver (Peer) Registration
The watcher derives the NTT transceiver emitter
`keccak256("ntt" + manager32 + token32)` — the **same** emitter used for
`NativeTokenTransfer` messages from this manager+token pair — and emits the VAA
under it, so the accountant keys hub/peer state on the same emitter it sees
transfers from.

#### Initialize Transceiver (kind = Hub)

Synthesize a `WormholeTransceiverInfo` payload (70 bytes):

```go
[4]byte prefix = 0x9c23bd3b
[32]byte manager_address // manager32 (left-padded XRPL account)
uint8 manager_mode // 0 = Locking, 1 = Burning (see below)
[32]byte token_address // token32 derived from token_id
uint8 token_decimals
```

`manager_mode` is derived by the watcher from the XREG `manager` and `token_id`:
an XRPL custody account is in **Burning** mode when it is itself the token's
issuer (it can mint/burn), and **Locking** mode otherwise (it holds a token
issued by another account). The issuer is the last 20 bytes of an IOU `token_id`,
or the first 20 bytes of an MPT `mpt_issuance_id`; XRP has no issuer and is always
Locking. The `ntt-global-accountant` only accepts Locking hubs, but the init
message must attest the manager's true mode rather than assume Locking.

#### Transceiver (Peer) Registration (kind = Peer)

Synthesize a `WormholeTransceiverRegistration` payload (38 bytes):

```go
[4]byte prefix = 0x18fc67c2
uint16 chain_id // big-endian, the peer chain
[32]byte transceiver_address // the peer address
```

> 🚧 TODO
An operator submits the resulting VAAs to the NTT global accountant via
`submit_vaas` to establish the hub/peer registration before transfers are
enforced.

### NativeTokenTransfer TransceiverMessage

Expand Down
Loading
Loading